Compare commits

...

148 Commits

Author SHA1 Message Date
Jeremy Stretch
6c1c695616 Merge pull request #6172 from netbox-community/develop
Release v2.10.10
2021-04-15 15:34:24 -04:00
Jeremy Stretch
8bdab347b6 Merge branch 'master' into develop 2021-04-15 15:24:36 -04:00
jeremystretch
ae349d4424 Release v2.10.10 2021-04-15 15:20:30 -04:00
jeremystretch
3e49745257 Changelog for #6168 2021-04-15 13:30:42 -04:00
Jeremy Stretch
c2b7226205 Merge pull request #6169 from promasu/develop
Fixes #6168: Add SFP56 interface type
2021-04-15 13:29:44 -04:00
jeremystretch
f67369a9a9 Fixes #5643: Fix VLAN assignment when editing VM interfaces in bulk 2021-04-15 13:26:19 -04:00
Adrian Nöthlich
14d2a49942 Fixes #6168: Add SFP56 interface type
Signed-off-by: Adrian Nöthlich <git@promasu.tech>
2021-04-15 18:37:45 +02:00
jeremystretch
92df40a6a0 Fixes #5652: Update object data when renaming a custom field 2021-04-15 12:04:34 -04:00
jeremystretch
a2203da1c6 Fixes #6162: Fix choice field filters (multiple models) 2021-04-14 16:17:19 -04:00
jeremystretch
3d0882856f Closes #5796: Add DC terminal power port, outlet types 2021-04-14 10:44:15 -04:00
jeremystretch
e849d28276 Closes #5980: Add Saf-D-Grid power port, outlet types 2021-04-14 10:40:31 -04:00
jeremystretch
18a691482d Closes #6160: Add F connector port type 2021-04-14 10:38:54 -04:00
jeremystretch
7c48e3632d Closes #6157: Support Markdown rendering for report logs 2021-04-14 10:21:07 -04:00
jeremystretch
cc433388f5 Fixes #6056: Optimize change log cleanup 2021-04-13 13:48:22 -04:00
jeremystretch
1fba4b7e32 Fixes #5419: Update parent device/VM when deleting a primary IP 2021-04-13 13:23:25 -04:00
jeremystretch
c249cd4ffd Fixes #6152: Fix custom field filtering for cables, virtual chassis 2021-04-13 12:05:44 -04:00
jeremystretch
d54bf5f75e Fixes #6144: Fix MAC address field display in VM interfaces search form 2021-04-13 11:52:32 -04:00
jeremystretch
9cbe3ff551 Enable close-stale-issue action 2021-04-13 11:46:14 -04:00
jeremystretch
3d8a3a2204 Fix link 2021-04-12 15:17:50 -04:00
jeremystretch
b19734004a Removed the "Additional information" blocks from issue templates (no longer needed) 2021-04-12 15:06:40 -04:00
jeremystretch
d43d5a6cb6 Change stale issues management to GitHub Actions 2021-04-12 13:53:38 -04:00
jeremystretch
99f4b2cf95 PRVB 2021-04-12 13:29:37 -04:00
Jeremy Stretch
b493d739bd Merge pull request #6143 from netbox-community/develop
Release v2.10.9
2021-04-12 13:28:28 -04:00
jeremystretch
92fb43a455 Merge branch 'master' into develop 2021-04-12 13:18:24 -04:00
jeremystretch
7c29fb449e Release v2.10.9 2021-04-12 13:10:33 -04:00
jeremystretch
7ddcec3a0d Fixes #6082: Support colons in webhook header values 2021-04-12 12:09:37 -04:00
jeremystretch
348fca7e28 Fixes #6117: Handle exception when attempting to assign an MPTT-enabled model as its own parent 2021-04-11 12:57:53 -04:00
jeremystretch
cc9b750eff Changelog & docs for #6083 2021-04-09 14:58:40 -04:00
Marcus Weiner
701ad8a4a9 Allow skipping TLS cert verification on Redis connection (#6084)
* Allow skipping redis tls cert verification

* Add config example
2021-04-09 14:51:58 -04:00
jeremystretch
2cc088c633 Fixes #6131: Correct handling of boolean fields when cloning objects 2021-04-09 14:42:07 -04:00
tcaiazza
4dfba3a2ad Update export-templates.md (#6091)
* Update export-templates.md

* Update export-templates.md

Co-authored-by: Jeremy Stretch <jeremy@netverity.dev>
2021-04-09 14:14:08 -04:00
jeremystretch
e69251b21a Fixes #6070: Add missing 'count_ipaddresses' attribute to VMInterface serializer 2021-04-08 14:22:45 -04:00
jeremystretch
f096c4a5d0 #6081: Tweak queryset filtering 2021-04-08 14:18:07 -04:00
jeremystretch
03b3f5937f Fixes #6108: Do not infer tenant assignment from parent objects for prefixes, IP addresses 2021-04-08 13:50:06 -04:00
jeremystretch
ae3527df16 Fixes #6081: Fix interface connections REST API endpoint 2021-04-07 16:04:32 -04:00
jeremystretch
85d0270af0 Fixes #6099: Correct example permission description 2021-04-07 15:50:24 -04:00
jeremystretch
d42b0691b2 Fix 'select all' widget 2021-04-05 17:13:32 -04:00
Jeremy Stretch
7bd853e87b Fixes #5805: Fix missing custom field filters for cables, rack reservations 2021-03-31 17:02:21 -04:00
Jeremy Stretch
861a52d27c Closes #5965: Mention cf property on CustomFieldModel in docs 2021-03-31 16:35:28 -04:00
Jeremy Stretch
9df2130e11 Closes #5840: Add column to cable termination objects to display cable color 2021-03-31 15:49:29 -04:00
Jeremy Stretch
6242e195be Closes #5756: Omit child devices from non-racked devices list under rack view 2021-03-31 15:33:06 -04:00
Jeremy Stretch
f2f0ea8d04 Closes #5526: Add MAC address search field to VM interfaces list 2021-03-31 15:27:38 -04:00
Jeremy Stretch
b7309d5c69 Closes #6054: Display NAPALM-enabled device tabs only when relevant 2021-03-31 15:21:07 -04:00
Jeremy Stretch
6ec8ac7597 Fixes #6073: Permit users to manage their own REST API tokens without needing explicit permission 2021-03-31 13:25:06 -04:00
Jeremy Stretch
3d286fbdc3 Merge pull request #6061 from netbox-community/develop
Release v2.10.8
2021-03-26 10:22:42 -04:00
Jeremy Stretch
c8eae3a5c3 PRVB 2021-03-26 10:11:57 -04:00
Jeremy Stretch
afc58e6bff Merge branch 'master' into develop 2021-03-26 10:11:04 -04:00
Jeremy Stretch
f59b5119e5 Release v2.10.8 2021-03-26 10:04:00 -04:00
Jeremy Stretch
8eca7377a5 Fixes #6060: Fix exception on cable trace in UI 2021-03-26 09:40:51 -04:00
Jeremy Stretch
e639de9861 PRVB 2021-03-25 15:36:32 -04:00
Jeremy Stretch
9a4d885a9c Merge pull request #6055 from netbox-community/develop
Release v2.10.7
2021-03-25 15:34:45 -04:00
Jeremy Stretch
d28cc4fdb5 Merge branch 'master' into develop 2021-03-25 15:23:43 -04:00
Jeremy Stretch
54a979c936 Release v2.10.7 2021-03-25 15:15:36 -04:00
Jeremy Stretch
d18652f726 Fixes #6012: Pre-populate attributes when creating an available child prefix via the UI 2021-03-25 15:04:56 -04:00
Jeremy Stretch
ac1083f65d Replace checkboxes in documentation issue template with dropdown 2021-03-25 12:03:00 -04:00
Jeremy Stretch
bcfc2e5f74 Changelog & tweaks for #5650 2021-03-25 11:51:02 -04:00
Jeremy Stretch
a135396d7b Merge pull request #5672 from cpmills1975/5650-render-incomplete-lengths
Indicate when cable length is not definitive
2021-03-25 11:44:00 -04:00
Jeremy Stretch
ef987bd008 Closes #5723: Allow customization of the geographic mapping service via MAPS_URL config parameter 2021-03-25 11:34:24 -04:00
Jeremy Stretch
a2fef1df83 Closes #5748: Extend example pg_dump commands 2021-03-25 11:23:55 -04:00
Jeremy Stretch
016c7d4e6f Changelog for #5736 2021-03-25 11:11:11 -04:00
Jeremy Stretch
0a60bcdb8d Merge pull request #6028 from rodvand/develop
Closes #5736: Add Site as editable field in bulk editing of Device
2021-03-25 11:08:00 -04:00
Jeremy Stretch
5374e6c881 Closes #6040: Add UI search fields for asset tag for devices and racks 2021-03-24 15:05:19 -04:00
Jeremy Stretch
ab65ab860f Closes #5641: Allow filtering device components by label 2021-03-24 14:48:38 -04:00
Jeremy Stretch
ad2742dbb2 Fixes #6023: Fix display of bottom banner with uBlock Origin enabled 2021-03-24 14:25:09 -04:00
Jeremy Stretch
997077fc5d Merge pull request #6029 from maximumG/origin/6010-vc-duplicated
Closes #6010: Avoid duplicate entries when searching for VC
2021-03-24 14:16:23 -04:00
maxime-gerges-external
c8042ad511 Closes #6010: Avoid duplicate entries when searching for VC 2021-03-22 11:15:58 +01:00
Martin Rødvand
b51d66ea72 Closes #5736: Add Site as editable field in bulk editing of Device 2021-03-22 11:15:17 +01:00
Jeremy Stretch
0321ae0a7f Merge pull request #6005 from Daimler/use-relative-links
Closes #5523: Use relative links that work locally and in production
2021-03-19 10:38:23 -04:00
Jeremy Stretch
e4ecd2dae9 Fixes #6006: Fix VLAN group/site association for bulk prefix import 2021-03-19 10:22:30 -04:00
Marcus Crane
335cfcde57 Use relative links that work locally and in production
At present, a mix of link types are used in the Netbox
documentation from markdown file links to relative and
absolute anchor links.

Of the three types, linking to markdown files is the
most ideal because it allows navigation locally on disk,
as well as being translated into working links at render
time.

While not obvious, mkdocs handles converting markdown
links to valid URLs.

Signed-Off-by: Marcus Crane <marcu.crane@daimler.com>
2021-03-18 19:39:03 +13:00
Jeremy Stretch
91fe80f73c Changelog for #5977 2021-03-17 15:41:27 -04:00
Jeremy Stretch
7e437455c1 Merge pull request #5978 from jathanism/jathanism-fix-invalid-release_check_url
Fix use of `URLValidator` to correctly validate `RELEASE_CHECK_URL`
2021-03-17 15:29:50 -04:00
Jeremy Stretch
f9f1a4439f Update Slack links 2021-03-15 14:45:53 -04:00
jathanism
68082a88a7 Fix use of URLValidator to correctly validate RELEASE_CHECK_URL 2021-03-13 11:39:21 -08:00
Jeremy Stretch
a94e5c7403 Changelog for #5953 and #5966 2021-03-12 10:59:23 -05:00
Jeremy Stretch
c061f7021e Merge pull request #5964 from rodvand/develop
Closes #5953: Adds Markdown rendering of Custom Scripts' descriptions
2021-03-12 10:56:33 -05:00
Jeremy Stretch
ad43373f5b Merge pull request #5967 from abrahamvegh/tabindex
Skip the cheatsheet link when tabbing
2021-03-12 10:54:03 -05:00
Abraham Vegh
720b88048d Skip the cheatsheet link when tabbing 2021-03-11 22:40:03 -05:00
Martin Rødvand
d58291d119 Closes #5953: Adds Markdown rendering of Custom Scripts' descriptions 2021-03-11 22:27:43 +01:00
Jeremy Stretch
132b1ff479 Fixes #5962: Ensure consistent display of change log action labels 2021-03-11 13:42:26 -05:00
Jeremy Stretch
cb9478e0ea Closes #5950: Use TimeZoneSerializerField from django-timezone-field 2021-03-10 17:08:11 -05:00
Jeremy Stretch
04964cc52b Fixes #5595: Restore ability to delete an uploaded device type image 2021-03-10 17:00:35 -05:00
Jeremy Stretch
9990fd25d4 PRVB 2021-03-09 17:16:22 -05:00
Jeremy Stretch
e9930854c4 Merge pull request #5949 from netbox-community/develop
Release v2.10.6
2021-03-09 17:13:23 -05:00
Jeremy Stretch
11ac4cd2c2 Merge branch 'master' into develop 2021-03-09 17:03:54 -05:00
Jeremy Stretch
5b07c77708 Release v2.10.6 2021-03-09 16:57:03 -05:00
Jeremy Stretch
c3ad2d0a80 Fixes #5948: Invalidate cached queries when running renaturalize 2021-03-09 16:39:55 -05:00
Jeremy Stretch
1cc82f2ab7 Create FUNDING.yml 2021-03-09 15:20:06 -05:00
Jeremy Stretch
358d7ac562 Closes #5942: Add button to add a new IP address on interface view 2021-03-08 17:16:07 -05:00
Jeremy Stretch
cae784ff52 Merge pull request #5928 from wobcom/feature/ip-actions-in-interface-view
Add IP action buttons to interface view
2021-03-08 16:58:53 -05:00
Jeremy Stretch
835e11d10f Fixes #5920: Revert #5760 (do not force database encoding) 2021-03-08 16:54:28 -05:00
Jeremy Stretch
e7157973e7 Fixes #5922: Fix options for filtering object permissions in admin UI 2021-03-08 16:51:11 -05:00
Jeremy Stretch
5b5110acbf Closes #5923: Include depdency upgrades as part of standard release process 2021-03-08 16:46:11 -05:00
Jeremy Stretch
61e5eff666 Fixes #5935: Fix filtering prefixes list by multiple prefix values 2021-03-08 16:27:12 -05:00
Jeremy Stretch
16a3d1339a Closes #5592: Add IP addresses count to VRF view 2021-03-08 15:57:17 -05:00
Jeremy Stretch
759190d8ba Retain issue body to allow file attachments 2021-03-08 15:16:56 -05:00
Jeremy Stretch
f8e2cb06d1 Retain issue body to allow file attachments 2021-03-08 15:15:37 -05:00
Jeremy Stretch
37aa10c5a5 Fix indentations 2021-03-08 14:59:21 -05:00
Jeremy Stretch
ad9ed13b9a Convert housekeeping and documentation templates to forms 2021-03-08 14:57:42 -05:00
Jeremy Stretch
d9f8d809c8 Add introductory text for FRs and bug reports 2021-03-08 14:49:26 -05:00
Jeremy Stretch
03054b88aa Fix 'steps to reproduce' placeholder and required fields 2021-03-08 14:40:09 -05:00
Jeremy Stretch
a72911b527 Fix indentations 2021-03-08 14:37:11 -05:00
Jeremy Stretch
8e4da3faf4 Convert bug report template to issue form 2021-03-08 14:35:37 -05:00
Jeremy Stretch
46945aa98d Fix FR form 2021-03-08 14:25:20 -05:00
Jeremy Stretch
939224d0af Convert feature request template to issue form 2021-03-08 14:22:04 -05:00
Jeremy Stretch
8d15f79f1e Merge pull request #5931 from candlerb/candlerb/doc-django-rq
Doc fix: navigation to "Django RQ"
2021-03-08 09:21:50 -05:00
Brian Candler
b079cc12f4 Doc: give correct navigation to the "Django RQ" section 2021-03-07 17:25:49 +00:00
Julian Jacobi
ad1da22257 Add IP action buttons to interface view 2021-03-05 10:45:58 +01:00
Jeremy Stretch
fa8e70fe26 Update changelog 2021-03-01 16:07:44 -05:00
Jeremy Stretch
a87f34ebb3 Merge pull request #5820 from stuntguy3000/develop
Fix Primary IP Sorting Issues for Devices and VMs
2021-03-01 16:04:43 -05:00
Jeremy Stretch
75d875615b Merge pull request #5897 from wobcom/feature/device_components_custom_links
Activate custom links on device components
2021-03-01 16:01:35 -05:00
Daniel Sheppard
f63cf6a7a4 Update changelog 2021-03-01 09:21:57 -06:00
niels
18ab1144bb Fixes: #5703 - Populate VRF and Tenant fields when adding IP address 2021-03-01 09:19:50 -06:00
Jeremy Stretch
a9b216d212 Update Slack links; start phasing out mailing list 2021-03-01 09:54:30 -05:00
Daniel Sheppard
6b19a1ece9 Update changelog 2021-03-01 08:52:13 -06:00
Last Wednesday
0dd6d552a8 Fixes: #5630 - Add QSFP+ (64GFC) FibreChannel Interface option
* Added option for 64GFC QSFP+ Interface for FibreChannel in netbox/dcim/choices.py
2021-03-01 08:48:40 -06:00
Julian Jacobi
cfddf570b9 add custom links to device components 2021-03-01 09:30:43 +01:00
Luke Anderson
d2c8aae59c Fix #5819 and #5872 - Fix Primary IP Sorting Issues for Devices and VMs 2021-02-25 22:32:51 +10:30
Jeremy Stretch
b1cd634ab4 PRVB 2021-02-24 15:38:36 -05:00
Jeremy Stretch
47abd62c55 Merge pull request #5865 from netbox-community/develop
Release v2.10.5
2021-02-24 15:36:29 -05:00
Jeremy Stretch
db781437fc Release v2.10.5 2021-02-24 15:15:33 -05:00
Jeremy Stretch
a40f52ee62 Changelog for #5753 2021-02-24 15:02:13 -05:00
Jeremy Stretch
89e6fd68e5 Merge pull request #5753 from nerzhul/patch-1
fix: add missing password when using redis in sentinel mode
2021-02-24 15:01:03 -05:00
Jeremy Stretch
ecf0f15c17 Merge pull request #5763 from candlerb/candlerb/5760
Update docs to to create database explicitly with UTF8 encoding.
2021-02-24 14:57:50 -05:00
Jeremy Stretch
04a6e2de9d Changelog and test for #5786 2021-02-24 14:39:09 -05:00
Jeremy Stretch
0cd29daea2 Merge pull request #5787 from pgnuta/patch-1
Update /netbox/tenancy/api/serializers.py to allow nullable group
2021-02-24 14:29:11 -05:00
Jeremy Stretch
b392502b9b Fixes #5841: Disallow the creation of available prefixes/IP addresses in violation of assigned permission constraints 2021-02-24 14:21:42 -05:00
Jeremy Stretch
a301c974e4 Merge pull request #5851 from cpmills1975/5847-pagination-font-error
Fixes: #5847 - Provide custom CSS for pagination chevrons
2021-02-23 11:37:35 -05:00
Chris Mills
4c7c2edf9a Fixes: #5847 - Provide custom CSS for pagination chevrons 2021-02-22 23:28:30 +00:00
Daniel Sheppard
3d3748d6f5 Fixes: #5315 - Make "null_option" on DynamicModelChoiceField also null the value on the model. (#5704)
Fixes: #5315 - Fix site unassignment from VLAN when using "None" option
2021-02-12 10:53:40 -06:00
pgnuta
fa3199d41c Update serializers.py
Group should be nullable via API to match frontend functionality and Swagger documentation.
2021-02-11 11:49:57 +13:00
Jeremy Stretch
efbda6d5af Apply stale rules to PRs 2021-02-09 15:15:49 -05:00
Jeremy Stretch
8640f500d1 Closes #5776: Upgrade pip when running upgrade.sh 2021-02-09 13:32:29 -05:00
Jeremy Stretch
3d90e3aee9 Fixes #5626: Fix REST API representation for circuit terminations connected to non-interface endpoints 2021-02-08 16:44:04 -05:00
Jeremy Stretch
6c676d21c3 Changelog for #5735 2021-02-08 15:09:20 -05:00
Jeremy Stretch
7e6af88966 Merge pull request #5761 from candlerb/candlerb/5735
Fixes #5735: enforcement of duplicate IP address detection with roles
2021-02-08 15:07:53 -05:00
Brian Candler
3b53cf5e84 Update docs to to create database explicitly with UTF8 encoding.
Fixes #5760
2021-02-07 19:46:30 +00:00
root
713f02ca3f Fixes #5735: enforcement of duplicate IP address detection with roles 2021-02-07 10:31:56 +00:00
Jeremy Stretch
19844e81d1 Merge pull request #5754 from ypid/fix/branding
NetBox should always be referred to as NetBox
2021-02-04 16:01:58 -05:00
Robin Schneider
5fbe766a0a NetBox should always be referred to as NetBox
Fix all instances of "Netbox" except the one that is used as an example
how not to write it.

Ref: docs/development/style-guide.md
2021-02-04 21:39:55 +01:00
Jeremy Stretch
1430c0a6e6 Fixes #5738: Fix redirect to device components view after disconnecting a cable 2021-02-04 13:19:42 -05:00
Jeremy Stretch
e3e928f1c4 Fixes #5718: Fix bulk editing of services when no port(s) are defined 2021-02-04 13:01:55 -05:00
Jeremy Stretch
e155acbbd4 Merge pull request #5732 from candlerb/candlerb/sample-report-connection-status
Fix sample report in documentation
2021-02-04 11:47:18 -05:00
Loïc Blot
be1b6b6aa3 fix: add missing password when using redis in sentinel mode 2021-02-04 16:50:07 +01:00
Jeremy Stretch
b4ba5cbb7a Fixes #5716: Fix filtering rack reservations by custom field 2021-02-02 11:49:38 -05:00
Brian Candler
f28474d86e Fix sample report in documentation
Raised in #5729
2021-02-02 08:16:35 +00:00
Jeremy Stretch
1964073072 PRVB 2021-01-26 16:02:14 -05:00
Chris Mills
02e8979178 Changes to template, view and CablePath class to
indicate to users whether the cable length is
accurate or not.
2021-01-22 16:45:08 +00:00
101 changed files with 1154 additions and 607 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [jeremystretch]

View File

@@ -1,40 +0,0 @@
---
name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for reporting reproducible bugs. If you need assistance
with NetBox installation, or if you have a general question, please start a
discussion instead: https://github.com/netbox-community/netbox/discussions
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version:
* NetBox version:
<!--
Describe in detail the exact steps that someone else can take to reproduce
this bug using the current stable release of NetBox. Begin with the
creation of any necessary database objects and call out every operation
being performed explicitly. If reporting a bug in the REST API, be sure to
reconstruct the raw HTTP request(s) being made: Don't rely on a client
library such as pynetbox.
-->
### Steps to Reproduce
1.
2.
3.
<!-- What did you expect to happen? -->
### Expected Behavior
<!-- What happened instead? -->
### Observed Behavior

58
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,58 @@
---
name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"]
body:
- type: markdown
attributes:
value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a
current NetBox installation. If you're having trouble with installation or just
looking for assistance with using NetBox, please visit our
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead."
- type: input
attributes:
label: NetBox version
description: "What version of NetBox are you currently running?"
placeholder: v2.10.4
validations:
required: true
- type: dropdown
attributes:
label: Python version
description: "What version of Python are you currently running?"
options:
- 3.6
- 3.7
- 3.8
- 3.9
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: "Describe in detail the exact steps that someone else can take to
reproduce this bug using the current stable release of NetBox. Begin with the
creation of any necessary database objects and call out every operation being
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
the raw HTTP request(s) being made: Don't rely on a client library such as
pynetbox."
placeholder: |
1. Click on "create widget"
2. Set foo to 12 and bar to G
3. Click the "create" button
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: "What did you expect to happen?"
placeholder: "A new widget should have been created with the specified attributes"
validations:
required: true
- type: textarea
attributes:
label: Observed Behavior
description: "What happened instead?"
placeholder: "A TypeError exception was raised"
validations:
required: true

View File

@@ -1,28 +0,0 @@
---
name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
Please indicate the nature of the change by placing an X in one of the
boxes below.
-->
### Change Type
[ ] Addition
[ ] Correction
[ ] Deprecation
[ ] Cleanup (formatting, typos, etc.)
### Area
[ ] Installation instructions
[ ] Configuration parameters
[ ] Functionality/features
[ ] REST API
[ ] Administration/development
[ ] Other
<!-- Describe the proposed change(s). -->
### Proposed Changes

View File

@@ -0,0 +1,35 @@
---
name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation
labels: ["type: documentation"]
body:
- type: dropdown
attributes:
label: Change Type
description: What type of change are you proposing?
options:
- Addition
- Correction
- Removal
- Cleanup (formatting, typos, etc.)
validations:
required: true
- type: dropdown
attributes:
label: Area
description: To what section of the documentation does this change primarily pertain?
options:
- Installation instructions
- Configuration parameters
- Functionality/features
- REST API
- Administration/development
- Other
validations:
required: true
- type: textarea
attributes:
label: Proposed Changes
description: "Describe the proposed changes and why they are necessary"
validations:
required: true

View File

@@ -1,54 +0,0 @@
---
name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please start a discussion instead:
https://github.com/netbox-community/netbox/discussions
NOTE: Due to an excessive backlog of feature requests, we are not currently
accepting any proposals which significantly extend NetBox's feature scope.
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version:
* NetBox version:
<!--
Describe in detail the new functionality you are proposing. Include any
specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
<!--
Convey an example use case for your proposed feature. Write from the
perspective of a NetBox user who would benefit from the proposed
functionality and describe how.
--->
### Use Case
<!--
Note any changes to the database schema necessary to support the new
feature. For example, does the proposal require adding a new model or
field? (Not all new features require database changes.)
--->
### Database Changes
<!--
List any new dependencies on external libraries or services that this new
feature would introduce. For example, does the proposal require the
installation of a new Python package? (Not all new features introduce new
dependencies.)
-->
### External Dependencies

View File

@@ -0,0 +1,53 @@
---
name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement
labels: ["type: feature"]
body:
- type: markdown
attributes:
value: "**NOTE:** This form is only for submitting well-formed proposals to extend or
modify NetBox in some way. If you're trying to solve a problem but can't figure out how,
or if you still need time to work on the details of a proposed new feature, please start
a [discussion](https://github.com/netbox-community/netbox/discussions) instead."
- type: input
attributes:
label: NetBox version
description: "What version of NetBox are you currently running?"
placeholder: v2.10.4
validations:
required: true
- type: dropdown
attributes:
label: Feature type
options:
- Data model extension
- New functionality
- Change to existing functionality
validations:
required: true
- type: textarea
attributes:
label: Proposed functionality
description: "Describe in detail the new feature or behavior you'd like to propose.
Include any specific changes to work flows, data models, or the user interface."
validations:
required: true
- type: textarea
attributes:
label: Use case
description: "Explain how adding this functionality would benefit NetBox users. What
need does it address?"
validations:
required: true
- type: textarea
attributes:
label: Database changes
description: "Note any changes to the database schema necessary to support the new
feature. For example, does the proposal require adding a new model or field? (Not
all new features require database changes.)"
- type: textarea
attributes:
label: External dependencies
description: "List any new dependencies on external libraries or services that this
new feature would introduce. For example, does the proposal require the installation
of a new Python package? (Not all new features introduce new dependencies.)"

View File

@@ -1,16 +0,0 @@
---
name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only)
---
<!--
NOTE: This template is for use by maintainers only. Please do not submit
an issue using this template unless you have been specifically asked to
do so.
-->
### Proposed Changes
<!-- Provide justification for the proposed change(s). -->
### Justification

View File

@@ -0,0 +1,22 @@
---
name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only)
labels: ["type: housekeeping"]
body:
- type: markdown
attributes:
value: "**NOTE:** This template is for use by maintainers only. Please do not submit
an issue using this template unless you have been specifically asked to do so."
- type: textarea
attributes:
label: Proposed Changes
description: "Describe in detail the new feature or behavior you'd like to propose.
Include any specific changes to work flows, data models, or the user interface."
validations:
required: true
- type: textarea
attributes:
label: Justification
description: "Please provide justification for the proposed change(s)."
validations:
required: true

33
.github/stale.yml vendored
View File

@@ -1,33 +0,0 @@
# Configuration for Stale (https://github.com/apps/stale)
# Pull requests are exempt from being marked as stale
only: issues
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 45
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 15
# Issues with these labels will never be considered stale
exemptLabels:
- "status: accepted"
- "status: blocked"
- "status: needs milestone"
# Label to use when marking an issue as stale
staleLabel: "pending closure"
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
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).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.

34
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
on:
schedule:
- cron: '0 4 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.
close-pr-message: >
This PR has been automatically closed due to lack of activity.
days-before-stale: 45
days-before-close: 15
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
remove-stale-when-updated: false
stale-issue-label: 'pending closure'
stale-issue-message: >
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).
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
recent activity. It will be closed automatically if no further action is
taken.

View File

@@ -25,7 +25,7 @@ discussions.
### Slack
For real-time chat, you can join the **#netbox** Slack channel on [NetworkToCode](https://slack.networktocode.com/).
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://slack.netbox.dev/).
Unfortunately, the Slack channel does not provide long-term retention of chat
history, so try to avoid it for any discussions would benefit from being
preserved for future reference.
@@ -185,11 +185,5 @@ overlooked.
sync to review agenda items. This meeting provides opportunity to present and
discuss pressing topics. Meetings are held as virtual audio/video conferences.
* Official channels for communication include:
* GitHub issues, pull requests, and discussions
* The [netbox-discuss](https://groups.google.com/g/netbox-discuss) mailing list
* The **#netbox** channel on [NetworkToCode Slack](https://networktocode.slack.com/)
* Maintainers with no substantial recorded activity in a 60-day period will be
removed from the project.

View File

@@ -12,7 +12,11 @@ complete list of requirements, see `requirements.txt`. The code is available [on
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/).
Questions? Comments? Start by perusing our [GitHub discussions](https://github.com/netbox-community/netbox/discussions) for the topic you have in mind.
### Discussion
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Build Status

View File

@@ -1,6 +1,6 @@
# Caching
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../../configuration/optional-settings/#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../configuration/optional-settings.md#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database.

View File

@@ -39,6 +39,12 @@ Each custom selection field must have at least two choices. These are specified
If a default value is specified for a selection field, it must exactly match one of the provided choices.
## Custom Fields in Templates
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`.
## Custom Fields and the REST API
When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined:

View File

@@ -18,6 +18,14 @@ Height: {{ rack.u_height }}U
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
```
{% for server in queryset %}
{% set data = server.get_config_context() %}
{{ data.syslog }}
{% endfor %}
```
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
## Example

View File

@@ -2,6 +2,13 @@
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission
!!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.
@@ -22,7 +29,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment
## Authentication
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
By default, the [`NAPALM_USERNAME`](../configuration/optional-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/optional-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \

View File

@@ -26,4 +26,4 @@ For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on you
When deploying NetBox in a multiprocess manner (e.g. running multiple Gunicorn workers) the Prometheus client library requires the use of a shared directory to collect metrics from all worker processes. To configure this, first create or designate a local directory to which the worker processes have read and write access, and then configure your WSGI service (e.g. Gunicorn) to define this path as the `prometheus_multiproc_dir` environment variable.
!!! warning
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).

View File

@@ -12,7 +12,7 @@ A NetBox report is a mechanism for validating the integrity of data within NetBo
## Writing Reports
Reports must be saved as files in the [`REPORTS_ROOT`](../../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
!!! warning
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
@@ -66,7 +66,7 @@ class DeviceConnectionsReport(Report):
for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint is not None:
connected_ports += 1
if not power_port.connection_status:
if not power_port.path.is_active:
self.log_warning(
device,
"Power connection for {} marked as planned".format(power_port.name)

View File

@@ -68,7 +68,7 @@ If no body template is specified, the request body will be populated with a JSON
## Webhook Processing
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues.
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.

View File

@@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
| ----------- | ----------- |
| `{"status": "active"}` | Status is active |
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing |
| `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing |
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |

View File

@@ -12,13 +12,16 @@ NetBox employs a [PostgreSQL](https://www.postgresql.org/) database, so general
Use the `pg_dump` utility to export the entire database to a file:
```no-highlight
pg_dump netbox > netbox.sql
pg_dump --username netbox --password --host localhost netbox > netbox.sql
```
!!! note
You may need to change the username, host, and/or database in the command above to match your installation.
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
```no-highlight
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
pg_dump ... --exclude-table-data=extras_objectchange netbox > netbox.sql
```
### Load an Exported Database
@@ -41,7 +44,7 @@ Keep in mind that PostgreSQL user accounts and permissions are not included with
If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following:
```no-highlight
pg_dump -s netbox > netbox_schema.sql
pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.sql
```
---

View File

@@ -281,6 +281,14 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
---
## MAPS_URL
Default: `https://maps.google.com/?q=` (Google Maps)
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
---
## MAX_PAGE_SIZE
Default: 1000
@@ -301,7 +309,7 @@ The file path to the location where media files (such as image attachments) are
Default: False
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../../additional-features/prometheus-metrics/) documentation for more details.
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details.
---

View File

@@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
* `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis
* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended)
An example configuration is provided below:

View File

@@ -5,8 +5,8 @@
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
* A Linux system or environment
* A PostgreSQL server, which can be installed locally [per the documentation](/installation/1-postgresql/)
* A Redis server, which can also be [installed locally](/installation/2-redis/)
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
* A supported version of Python
### Fork the Repo

View File

@@ -4,12 +4,12 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
## Communication
Communication among developers should always occur via public channels:
There are several official forums for communication among the developers and community members:
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [The mailing list](https://groups.google.com/g/netbox-discuss) - An alternative forum for general discussion (GitHub is preferred).
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://slack.netbox.dev/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
## Governance

View File

@@ -2,24 +2,9 @@
## Minor Version Bumps
### Update Requirements
### Address Pinned Dependencies
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
```
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
```
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
Every minor version release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
1. Create a new virtual environment.
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
3. Run all tests and check that the UI and API function as expected.
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
5. Update the package versions in `requirements.txt` as appropriate.
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
### Update Static Libraries
@@ -58,6 +43,27 @@ Submit a pull request to merge the `feature` branch into the `develop` branch in
## All Releases
### Update Requirements
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
```
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
```
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
1. Create a new virtual environment.
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
3. Run all tests and check that the UI and API function as expected.
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
5. Update the package versions in `requirements.txt` as appropriate.
In cases where upgrading a dependency to its most recent release is breaking, it should be pinned to its current minor version in `base_requirements.txt` (with an explanatory comment) and revisited for the next major NetBox release.
### Verify CI Build Status
Ensure that continuous integration testing on the `develop` branch is completing successfully.

View File

@@ -113,7 +113,7 @@ cd /opt/netbox/netbox/netbox/
sudo cp configuration.example.py configuration.py
```
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](/configuration/), but only the following four are required for new installations:
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
* `ALLOWED_HOSTS`
* `DATABASE`
@@ -136,7 +136,7 @@ ALLOWED_HOSTS = ['*']
### DATABASE
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](/configuration/required-settings/#database) for more detail on individual parameters.
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-settings.md#database) for more detail on individual parameters.
```python
DATABASE = {
@@ -151,7 +151,7 @@ DATABASE = {
### REDIS
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](/configuration/required-settings/#redis) for more detail on individual parameters.
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-settings.md#redis) for more detail on individual parameters.
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
@@ -203,7 +203,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt
### Remote File Storage
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](/configuration/optional-settings/#storage_backend) in `configuration.py`.
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
```no-highlight
sudo echo django-storages >> /opt/netbox/local_requirements.txt

View File

@@ -140,9 +140,9 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
## Troubleshooting LDAP
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
`systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`:
For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/optional-settings.md#logging) configuration to `configuration.py`:
```python
LOGGING = {

View File

@@ -2,7 +2,7 @@
## Review the Release Notes
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
## Update Dependencies to Required Versions

View File

@@ -1,5 +1,119 @@
# NetBox v2.10
## v2.10.10 (2021-04-15)
### Enhancements
* [#5796](https://github.com/netbox-community/netbox/issues/5796) - Add DC terminal power port, outlet types
* [#5980](https://github.com/netbox-community/netbox/issues/5980) - Add Saf-D-Grid power port, outlet types
* [#6157](https://github.com/netbox-community/netbox/issues/6157) - Support Markdown rendering for report logs
* [#6160](https://github.com/netbox-community/netbox/issues/6160) - Add F connector port type
* [#6168](https://github.com/netbox-community/netbox/issues/6168) - Add SFP56 50GE interface type
### Bug Fixes
* [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP
* [#5643](https://github.com/netbox-community/netbox/issues/5643) - Fix VLAN assignment when editing VM interfaces in bulk
* [#5652](https://github.com/netbox-community/netbox/issues/5652) - Update object data when renaming a custom field
* [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup
* [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form
* [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis
* [#6162](https://github.com/netbox-community/netbox/issues/6162) - Fix choice field filters (multiple models)
---
## v2.10.9 (2021-04-12)
### Enhancements
* [#5526](https://github.com/netbox-community/netbox/issues/5526) - Add MAC address search field to VM interfaces list
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis
### Bug Fixes
* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint
* [#6082](https://github.com/netbox-community/netbox/issues/6082) - Support colons in webhook header values
* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses
* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent
* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects
---
## v2.10.8 (2021-03-26)
### Bug Fixes
* [#6060](https://github.com/netbox-community/netbox/issues/6060) - Fix exception on cable trace in UI (regression from #5650)
---
## v2.10.7 (2021-03-25)
### Enhancements
* [#5641](https://github.com/netbox-community/netbox/issues/5641) - Allow filtering device components by label
* [#5723](https://github.com/netbox-community/netbox/issues/5723) - Allow customization of the geographic mapping service via `MAPS_URL` config parameter
* [#5736](https://github.com/netbox-community/netbox/issues/5736) - Allow changing site assignment when bulk editing devices
* [#5953](https://github.com/netbox-community/netbox/issues/5953) - Support Markdown rendering for custom script descriptions
* [#6040](https://github.com/netbox-community/netbox/issues/6040) - Add UI search fields for asset tag for devices and racks
### Bug Fixes
* [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image
* [#5650](https://github.com/netbox-community/netbox/issues/5650) - Denote when the total length of a cable trace may exceed the indicated value
* [#5962](https://github.com/netbox-community/netbox/issues/5962) - Ensure consistent display of change log action labels
* [#5966](https://github.com/netbox-community/netbox/issues/5966) - Skip Markdown reference link when tabbing through form fields
* [#5977](https://github.com/netbox-community/netbox/issues/5977) - Correct validation of `RELEASE_CHECK_URL` config parameter
* [#6006](https://github.com/netbox-community/netbox/issues/6006) - Fix VLAN group/site association for bulk prefix import
* [#6010](https://github.com/netbox-community/netbox/issues/6010) - Eliminate duplicate virtual chassis search results
* [#6012](https://github.com/netbox-community/netbox/issues/6012) - Pre-populate attributes when creating an available child prefix via the UI
* [#6023](https://github.com/netbox-community/netbox/issues/6023) - Fix display of bottom banner with uBlock Origin enabled
---
## v2.10.6 (2021-03-09)
### Enhancements
* [#5592](https://github.com/netbox-community/netbox/issues/5592) - Add IP addresses count to VRF view
* [#5630](https://github.com/netbox-community/netbox/issues/5630) - Add QSFP+ (64GFC) FibreChannel Interface option
* [#5884](https://github.com/netbox-community/netbox/issues/5884) - Enable custom links for device components
* [#5914](https://github.com/netbox-community/netbox/issues/5914) - Add edit/delete buttons for IP addresses on interface view
* [#5942](https://github.com/netbox-community/netbox/issues/5942) - Add button to add a new IP address on interface view
### Bug Fixes
* [#5703](https://github.com/netbox-community/netbox/issues/5703) - Fix VRF and Tenant field population when adding IP addresses from prefix
* [#5819](https://github.com/netbox-community/netbox/issues/5819) - Enable ordering of virtual machines by primary IP address
* [#5872](https://github.com/netbox-community/netbox/issues/5872) - Ordering of devices by primary IP should respect `PREFER_IPV4` configuration parameter
* [#5922](https://github.com/netbox-community/netbox/issues/5922) - Fix options for filtering object permissions in admin UI
* [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values
* [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize`
---
## v2.10.5 (2021-02-24)
### Bug Fixes
* [#5315](https://github.com/netbox-community/netbox/issues/5315) - Fix site unassignment from VLAN when using "None" option
* [#5626](https://github.com/netbox-community/netbox/issues/5626) - Fix REST API representation for circuit terminations connected to non-interface endpoints
* [#5716](https://github.com/netbox-community/netbox/issues/5716) - Fix filtering rack reservations by custom field
* [#5718](https://github.com/netbox-community/netbox/issues/5718) - Fix bulk editing of services when no port(s) are defined
* [#5735](https://github.com/netbox-community/netbox/issues/5735) - Ensure consistent treatment of duplicate IP addresses
* [#5738](https://github.com/netbox-community/netbox/issues/5738) - Fix redirect to device components view after disconnecting a cable
* [#5753](https://github.com/netbox-community/netbox/issues/5753) - Fix Redis Sentinel password application for caching
* [#5786](https://github.com/netbox-community/netbox/issues/5786) - Allow setting null tenant group on tenant via REST API
* [#5841](https://github.com/netbox-community/netbox/issues/5841) - Disallow the creation of available prefixes/IP addresses in violation of assigned permission constraints
---
## v2.10.4 (2021-01-26)
### Enhancements

View File

@@ -20,7 +20,7 @@ http://netbox/api/dcim/sites/
}
```
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../../configuration/optional-settings/#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
```
$ curl http://netbox/api/dcim/sites/

View File

@@ -269,7 +269,7 @@ The brief format is supported for both lists and individual objects.
### Excluding Config Contexts
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
## Pagination
@@ -308,7 +308,7 @@ Vary: Accept
}
```
The default page is determined by the [`PAGINATE_COUNT`](../../configuration/optional-settings/#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
The default page is determined by the [`PAGINATE_COUNT`](../configuration/optional-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
```
http://netbox/api/dcim/devices/?limit=100
@@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a
}
```
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../../configuration/optional-settings/#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
!!! warning
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
@@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.'
### Creating a New Object
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](authentication.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
```no-highlight
curl -s -X POST \

View File

@@ -4,7 +4,7 @@ As with most other objects, the REST API can be used to view, create, modify, an
## Generating a Session Key
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`.
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../core-functionality/secrets.md#user-keys). The private key must be POSTed with the name `private_key`.
```no-highlight
$ curl -X POST http://netbox/api/secrets/get-session-key/ \

View File

@@ -40,14 +40,16 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
connected_endpoint = NestedInterfaceSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
fields = [
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint',
'connected_endpoint_type', 'connected_endpoint_reachable',
]
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):

View File

@@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import *
from dcim.constants import *
@@ -13,13 +14,12 @@ from dcim.models import (
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from dcim.utils import decompile_path_node
from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
from netbox.api import (
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer,
WritableNestedSerializer,
)
from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -98,7 +98,7 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
time_zone = TimeZoneSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
@@ -779,7 +779,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
interface_b = NestedInterfaceSerializer(source='_path.destination')
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
class Meta:

View File

@@ -2,6 +2,7 @@ import socket
from collections import OrderedDict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import F
from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404
@@ -580,6 +581,8 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.prefetch_related('device', '_path').filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
_path__destination_type__app_label='dcim',
_path__destination_type__model='interface',
_path__destination_id__isnull=False,
pk__lt=F('_path__destination_id')
)

View File

@@ -314,6 +314,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_USB_MICRO_B = 'usb-micro-b'
TYPE_USB_3_B = 'usb-3-b'
TYPE_USB_3_MICROB = 'usb-3-micro-b'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_SAF_D_GRID = 'saf-d-grid'
CHOICES = (
('IEC 60320', (
@@ -414,6 +418,12 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_USB_3_B, 'USB 3.0 Type B'),
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
)),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
)
@@ -507,8 +517,11 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_USB_A = 'usb-a'
TYPE_USB_MICROB = 'usb-micro-b'
TYPE_USB_C = 'usb-c'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
CHOICES = (
('IEC 60320', (
@@ -602,8 +615,12 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_USB_MICROB, 'USB Micro B'),
(TYPE_USB_C, 'USB Type C'),
)),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
)
@@ -645,6 +662,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_10GE_XENPAK = '10gbase-x-xenpak'
TYPE_10GE_X2 = '10gbase-x-x2'
TYPE_25GE_SFP28 = '25gbase-x-sfp28'
TYPE_50GE_SFP56 = '50gbase-x-sfp56'
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
TYPE_100GE_CFP = '100gbase-x-cfp'
@@ -686,6 +704,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
# InfiniBand
@@ -748,6 +767,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
(TYPE_10GE_X2, 'X2 (10GE)'),
(TYPE_25GE_SFP28, 'SFP28 (25GE)'),
(TYPE_50GE_SFP56, 'SFP56 (50GE)'),
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
(TYPE_100GE_CFP, 'CFP (100GE)'),
@@ -801,6 +821,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
)
),
@@ -879,6 +900,7 @@ class PortTypeChoices(ChoiceSet):
TYPE_TERA1P = 'tera-1p'
TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc'
TYPE_F = 'f'
TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st'
TYPE_SC = 'sc'
@@ -908,6 +930,7 @@ class PortTypeChoices(ChoiceSet):
(TYPE_TERA1P, 'TERA 1P'),
(TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'),
(TYPE_F, 'F Connector'),
(TYPE_MRJ21, 'MRJ21'),
),
),

View File

@@ -1,6 +1,5 @@
import django_filters
from django.contrib.auth.models import User
from django.db.models import Count
from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet
@@ -264,7 +263,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
)
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -447,6 +446,10 @@ class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta:
model = PowerOutletTemplate
@@ -454,6 +457,10 @@ class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
)
class Meta:
model = InterfaceTemplate
@@ -461,6 +468,10 @@ class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta:
model = FrontPortTemplate
@@ -468,6 +479,10 @@ class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta:
model = RearPortTemplate
@@ -783,7 +798,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
class Meta:
model = ConsolePort
fields = ['id', 'name', 'description']
fields = ['id', 'name', 'label', 'description']
class ConsoleServerPortFilterSet(
@@ -799,7 +814,7 @@ class ConsoleServerPortFilterSet(
class Meta:
model = ConsoleServerPort
fields = ['id', 'name', 'description']
fields = ['id', 'name', 'label', 'description']
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
@@ -810,7 +825,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
class Meta:
model = PowerPort
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description']
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
@@ -818,10 +833,14 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
choices=PowerOutletTypeChoices,
null_value=None
)
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta:
model = PowerOutlet
fields = ['id', 'name', 'feed_leg', 'description']
fields = ['id', 'name', 'label', 'feed_leg', 'description']
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
@@ -867,7 +886,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
class Meta:
model = Interface
fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
def filter_device(self, queryset, name, value):
try:
@@ -918,24 +937,32 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta:
model = FrontPort
fields = ['id', 'name', 'type', 'description']
fields = ['id', 'name', 'label', 'type', 'description']
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta:
model = RearPort
fields = ['id', 'name', 'type', 'positions', 'description']
fields = ['id', 'name', 'label', 'type', 'positions', 'description']
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
class Meta:
model = DeviceBay
fields = ['id', 'name', 'description']
fields = ['id', 'name', 'label', 'description']
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
@@ -996,7 +1023,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
class Meta:
model = InventoryItem
fields = ['id', 'name', 'part_id', 'asset_tag', 'discovered']
fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered']
def search(self, queryset, name, value):
if not value.strip():
@@ -1011,7 +1038,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
return queryset.filter(qs_filter)
class VirtualChassisFilterSet(BaseFilterSet):
class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -1075,10 +1102,10 @@ class VirtualChassisFilterSet(BaseFilterSet):
Q(members__name__icontains=value) |
Q(domain__icontains=value)
)
return queryset.filter(qs_filter)
return queryset.filter(qs_filter).distinct()
class CableFilterSet(BaseFilterSet):
class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -1302,6 +1329,10 @@ class PowerFeedFilterSet(
queryset=Rack.objects.all(),
label='Rack (ID)',
)
status = django_filters.MultipleChoiceFilter(
choices=PowerFeedStatusChoices,
null_value=None
)
tag = TagFilter()
class Meta:

View File

@@ -60,12 +60,18 @@ def get_device_by_name_or_pk(name):
class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
field_order = [
'q', 'region', 'site'
'q', 'name', 'label', 'region', 'site'
]
q = forms.CharField(
required=False,
label='Search'
)
name = forms.CharField(
required=False
)
label = forms.CharField(
required=False
)
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
@@ -659,7 +665,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Rack
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant', 'asset_tag']
q = forms.CharField(
required=False,
label='Search'
@@ -707,6 +713,9 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
required=False,
null_option='None'
)
asset_tag = forms.CharField(
required=False
)
tag = TagFilterField(model)
@@ -859,7 +868,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
nullable_fields = []
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = RackReservation
field_order = ['q', 'region', 'site', 'group_id', 'user_id', 'tenant_group', 'tenant']
q = forms.CharField(
@@ -944,10 +953,10 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
widgets = {
'subdevice_role': StaticSelect2(),
# Exclude SVG images (unsupported by PIL)
'front_image': forms.FileInput(attrs={
'front_image': forms.ClearableFileInput(attrs={
'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff'
}),
'rear_image': forms.FileInput(attrs={
'rear_image': forms.ClearableFileInput(attrs={
'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff'
})
}
@@ -2088,6 +2097,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
queryset=DeviceRole.objects.all(),
required=False
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
@@ -2117,7 +2130,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
model = Device
field_order = [
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
]
q = forms.CharField(
required=False,
@@ -2185,6 +2198,9 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
required=False,
widget=StaticSelect2Multiple()
)
asset_tag = forms.CharField(
required=False
)
mac_address = forms.CharField(
required=False,
label='MAC address'
@@ -3950,7 +3966,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFo
})
class CableFilterForm(BootstrapMixin, forms.Form):
class CableFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cable
q = forms.CharField(
required=False,

View File

@@ -480,17 +480,23 @@ class CablePath(models.Model):
def get_total_length(self):
"""
Return the sum of the length of each cable in the path.
Return a tuple containing the sum of the length of each cable in the path
and a flag indicating whether the length is definitive.
"""
cable_ids = [
# Starting from the first element, every third element in the path should be a Cable
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
]
return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
is_definitive = len(cables) == len(cable_ids)
return total_length, is_definitive
def get_split_nodes(self):
"""
Return all available next segments in a split cable path.
"""
rearport = path_node_to_object(self.path[-1])
return FrontPort.objects.filter(rear_port=rearport)

View File

@@ -198,7 +198,7 @@ class PathEndpoint(models.Model):
# Console ports
#
@extras_features('export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks', 'custom_links')
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -234,7 +234,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
# Console server ports
#
@extras_features('webhooks')
@extras_features('webhooks', 'custom_links')
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -270,7 +270,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
# Power ports
#
@extras_features('export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks', 'custom_links')
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -379,7 +379,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
# Power outlets
#
@extras_features('webhooks')
@extras_features('webhooks', 'custom_links')
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -478,8 +478,12 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
@extras_features('export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks', 'custom_links')
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@@ -615,16 +619,12 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
def is_lag(self):
return self.type == InterfaceTypeChoices.TYPE_LAG
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
#
# Pass-through ports
#
@extras_features('webhooks')
@extras_features('webhooks', 'custom_links')
class FrontPort(CableTermination, ComponentModel):
"""
A pass-through port on the front of a Device.
@@ -687,7 +687,7 @@ class FrontPort(CableTermination, ComponentModel):
})
@extras_features('webhooks')
@extras_features('webhooks', 'custom_links')
class RearPort(CableTermination, ComponentModel):
"""
A pass-through port on the rear of a Device.
@@ -740,7 +740,7 @@ class RearPort(CableTermination, ComponentModel):
# Device bays
#
@extras_features('webhooks')
@extras_features('webhooks', 'custom_links')
class DeviceBay(ComponentModel):
"""
An empty space within a Device which can house a child device
@@ -800,7 +800,7 @@ class DeviceBay(ComponentModel):
# Inventory items
#
@extras_features('export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks', 'custom_links')
class InventoryItem(MPTTModel, ComponentModel):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

View File

@@ -111,6 +111,12 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
def clean(self):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})
# Parent RackGroup (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")

View File

@@ -7,6 +7,7 @@ from timezone_field import TimeZoneField
from dcim.choices import *
from dcim.constants import *
from django.core.exceptions import ValidationError
from dcim.fields import ASNField
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
@@ -87,6 +88,15 @@ class Region(MPTTModel, ChangeLoggedModel):
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
def clean(self):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})
#
# Sites

View File

@@ -1,5 +1,6 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from django.conf import settings
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
@@ -127,11 +128,18 @@ class DeviceTable(BaseTable):
verbose_name='Type',
text=lambda record: record.device_type.display_name
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
if settings.PREFER_IPV4:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
else:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'
@@ -222,6 +230,11 @@ class CableTerminationTable(BaseTable):
cable = tables.Column(
linkify=True
)
cable_color = ColorColumn(
accessor='cable.color',
orderable=False,
verbose_name='Cable Color'
)
cable_peer = tables.TemplateColumn(
accessor='_cable_peer',
template_code=CABLETERMINATION,
@@ -247,7 +260,8 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
@@ -266,7 +280,8 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions'
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
row_attrs = {
@@ -281,7 +296,10 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags')
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
@@ -300,7 +318,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions'
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
row_attrs = {
@@ -317,7 +336,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
model = PowerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable',
'cable_peer', 'connection', 'tags',
'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@@ -337,8 +356,8 @@ class DevicePowerPortTable(PowerPortTable):
class Meta(DeviceComponentTable.Meta):
model = PowerPort
fields = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_peer',
'connection', 'tags', 'actions',
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
@@ -360,8 +379,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = PowerOutlet
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_peer',
'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
@@ -380,8 +399,8 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(DeviceComponentTable.Meta):
model = PowerOutlet
fields = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_peer', 'connection',
'tags', 'actions',
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
@@ -416,7 +435,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
model = Interface
fields = (
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
'tagged_vlans',
)
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
@@ -442,7 +462,8 @@ class DeviceInterfaceTable(InterfaceTable):
model = Interface
fields = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
@@ -469,7 +490,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
model = FrontPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
'cable_peer', 'tags',
'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
@@ -489,8 +510,8 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(DeviceComponentTable.Meta):
model = FrontPort
fields = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
'tags', 'actions',
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_color',
'cable_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
@@ -508,7 +529,10 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags')
fields = (
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
@@ -527,7 +551,8 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags', 'actions',
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer', 'tags',
'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',

View File

@@ -4,7 +4,6 @@ from django_tables2.utils import Accessor
from dcim.models import PowerFeed, PowerPanel
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn
from .devices import CableTerminationTable
from .template_code import POWERFEED_CABLE, POWERFEED_CABLETERMINATION
__all__ = (
'PowerFeedTable',
@@ -69,7 +68,7 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed
fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'cable', 'cable_peer', 'connection', 'available_power', 'tags',
'max_utilization', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power', 'tags',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@@ -95,6 +95,11 @@ CONSOLEPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -115,6 +120,11 @@ CONSOLESERVERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -135,6 +145,11 @@ POWERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -154,6 +169,11 @@ POWEROUTLET_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -172,6 +192,11 @@ INTERFACE_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif record.is_connectable and perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -193,6 +218,11 @@ FRONTPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@@ -216,6 +246,11 @@ REARPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>

View File

@@ -851,9 +851,8 @@ class PowerOutletTemplateTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_feed_leg(self):
# TODO: Support filtering for multiple values
params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class InterfaceTemplateTestCase(TestCase):
@@ -892,9 +891,8 @@ class InterfaceTemplateTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
# TODO: Support filtering for multiple values
params = {'type': InterfaceTypeChoices.TYPE_1GE_FIXED}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mgmt_only(self):
params = {'mgmt_only': 'true'}
@@ -946,9 +944,8 @@ class FrontPortTemplateTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
# TODO: Support filtering for multiple values
params = {'type': PortTypeChoices.TYPE_8P8C}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class RearPortTemplateTestCase(TestCase):
@@ -987,9 +984,8 @@ class RearPortTemplateTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
# TODO: Support filtering for multiple values
params = {'type': PortTypeChoices.TYPE_8P8C}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_positions(self):
params = {'positions': [1, 2]}
@@ -1488,9 +1484,9 @@ class ConsolePortTestCase(TestCase):
ConsoleServerPort.objects.bulk_create(console_server_ports)
console_ports = (
ConsolePort(device=devices[0], name='Console Port 1', description='First'),
ConsolePort(device=devices[1], name='Console Port 2', description='Second'),
ConsolePort(device=devices[2], name='Console Port 3', description='Third'),
ConsolePort(device=devices[0], name='Console Port 1', label='A', description='First'),
ConsolePort(device=devices[1], name='Console Port 2', label='B', description='Second'),
ConsolePort(device=devices[2], name='Console Port 3', label='C', description='Third'),
)
ConsolePort.objects.bulk_create(console_ports)
@@ -1507,6 +1503,10 @@ class ConsolePortTestCase(TestCase):
params = {'name': ['Console Port 1', 'Console Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1584,9 +1584,9 @@ class ConsoleServerPortTestCase(TestCase):
ConsolePort.objects.bulk_create(console_ports)
console_server_ports = (
ConsoleServerPort(device=devices[0], name='Console Server Port 1', description='First'),
ConsoleServerPort(device=devices[1], name='Console Server Port 2', description='Second'),
ConsoleServerPort(device=devices[2], name='Console Server Port 3', description='Third'),
ConsoleServerPort(device=devices[0], name='Console Server Port 1', label='A', description='First'),
ConsoleServerPort(device=devices[1], name='Console Server Port 2', label='B', description='Second'),
ConsoleServerPort(device=devices[2], name='Console Server Port 3', label='C', description='Third'),
)
ConsoleServerPort.objects.bulk_create(console_server_ports)
@@ -1603,6 +1603,10 @@ class ConsoleServerPortTestCase(TestCase):
params = {'name': ['Console Server Port 1', 'Console Server Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1680,9 +1684,9 @@ class PowerPortTestCase(TestCase):
PowerOutlet.objects.bulk_create(power_outlets)
power_ports = (
PowerPort(device=devices[0], name='Power Port 1', maximum_draw=100, allocated_draw=50, description='First'),
PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, description='Second'),
PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, description='Third'),
PowerPort(device=devices[0], name='Power Port 1', label='A', maximum_draw=100, allocated_draw=50, description='First'),
PowerPort(device=devices[1], name='Power Port 2', label='B', maximum_draw=200, allocated_draw=100, description='Second'),
PowerPort(device=devices[2], name='Power Port 3', label='C', maximum_draw=300, allocated_draw=150, description='Third'),
)
PowerPort.objects.bulk_create(power_ports)
@@ -1699,6 +1703,10 @@ class PowerPortTestCase(TestCase):
params = {'name': ['Power Port 1', 'Power Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1784,9 +1792,9 @@ class PowerOutletTestCase(TestCase):
PowerPort.objects.bulk_create(power_ports)
power_outlets = (
PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'),
PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'),
PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'),
PowerOutlet(device=devices[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'),
PowerOutlet(device=devices[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'),
PowerOutlet(device=devices[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'),
)
PowerOutlet.objects.bulk_create(power_outlets)
@@ -1803,14 +1811,17 @@ class PowerOutletTestCase(TestCase):
params = {'name': ['Power Outlet 1', 'Power Outlet 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_feed_leg(self):
# TODO: Support filtering for multiple values
params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
@@ -1879,12 +1890,12 @@ class InterfaceTestCase(TestCase):
Device.objects.bulk_create(devices)
interfaces = (
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
)
Interface.objects.bulk_create(interfaces)
@@ -1901,6 +1912,10 @@ class InterfaceTestCase(TestCase):
params = {'name': ['Interface 1', 'Interface 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
@@ -2016,12 +2031,12 @@ class FrontPortTestCase(TestCase):
RearPort.objects.bulk_create(rear_ports)
front_ports = (
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
FrontPort(device=devices[3], name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
FrontPort(device=devices[3], name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
FrontPort(device=devices[3], name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
)
FrontPort.objects.bulk_create(front_ports)
@@ -2038,10 +2053,13 @@ class FrontPortTestCase(TestCase):
params = {'name': ['Front Port 1', 'Front Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
# TODO: Test for multiple values
params = {'type': PortTypeChoices.TYPE_8P8C}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
@@ -2108,12 +2126,12 @@ class RearPortTestCase(TestCase):
Device.objects.bulk_create(devices)
rear_ports = (
RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4),
RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5),
RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6),
RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
)
RearPort.objects.bulk_create(rear_ports)
@@ -2130,10 +2148,13 @@ class RearPortTestCase(TestCase):
params = {'name': ['Rear Port 1', 'Rear Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
# TODO: Test for multiple values
params = {'type': PortTypeChoices.TYPE_8P8C}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_positions(self):
params = {'positions': [1, 2]}
@@ -2203,9 +2224,9 @@ class DeviceBayTestCase(TestCase):
Device.objects.bulk_create(devices)
device_bays = (
DeviceBay(device=devices[0], name='Device Bay 1', description='First'),
DeviceBay(device=devices[1], name='Device Bay 2', description='Second'),
DeviceBay(device=devices[2], name='Device Bay 3', description='Third'),
DeviceBay(device=devices[0], name='Device Bay 1', label='A', description='First'),
DeviceBay(device=devices[1], name='Device Bay 2', label='B', description='Second'),
DeviceBay(device=devices[2], name='Device Bay 3', label='C', description='Third'),
)
DeviceBay.objects.bulk_create(device_bays)
@@ -2217,6 +2238,10 @@ class DeviceBayTestCase(TestCase):
params = {'name': ['Device Bay 1', 'Device Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2283,9 +2308,9 @@ class InventoryItemTestCase(TestCase):
Device.objects.bulk_create(devices)
inventory_items = (
InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
)
for i in inventory_items:
i.save()
@@ -2306,6 +2331,10 @@ class InventoryItemTestCase(TestCase):
params = {'name': ['Inventory Item 1', 'Inventory Item 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_part_id(self):
params = {'part_id': ['1001', '1002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2696,9 +2725,8 @@ class PowerFeedTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
# TODO: Test for multiple values
params = {'status': PowerFeedStatusChoices.STATUS_ACTIVE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'status': [PowerFeedStatusChoices.STATUS_ACTIVE, PowerFeedStatusChoices.STATUS_FAILED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
params = {'type': PowerFeedTypeChoices.TYPE_PRIMARY}

View File

@@ -342,10 +342,11 @@ class RackView(generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
def get_extra_context(self, request, instance):
# Get 0U and child devices located within the rack
# Get 0U devices located within the rack
nonracked_devices = Device.objects.filter(
rack=instance,
position__isnull=True
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
@@ -2134,10 +2135,14 @@ class PathTraceView(generic.ObjectView):
else:
path = related_paths.first()
# Get the total length of the cable and whether the length is definitive (fully defined)
total_length, is_definitive = path.get_total_length() if path else (None, False)
return {
'path': path,
'related_paths': related_paths,
'total_length': path.get_total_length() if path else None,
'total_length': total_length,
'is_definitive': is_definitive
}

View File

@@ -1,3 +1,4 @@
from cacheops import invalidate_model
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
@@ -27,7 +28,7 @@ class Command(BaseCommand):
app_label, model_name = name.split('.')
except ValueError:
raise CommandError(
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
f"Invalid format: {name}. Models must be specified in the form app_label.ModelName."
)
try:
app_config = apps.get_app_config(app_label)
@@ -36,13 +37,13 @@ class Command(BaseCommand):
try:
model = app_config.get_model(model_name)
except LookupError:
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
raise CommandError(f"Unknown model: {app_label}.{model_name}")
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if not fields:
raise CommandError(
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
f"Invalid model: {app_label}.{model_name} does not employ natural ordering"
)
models.append(
(model, fields)
@@ -67,7 +68,7 @@ class Command(BaseCommand):
models = self._get_models(args)
if options['verbosity']:
self.stdout.write("Renaturalizing {} models.".format(len(models)))
self.stdout.write(f"Renaturalizing {len(models)} models.")
for model, fields in models:
for field in fields:
@@ -78,7 +79,7 @@ class Command(BaseCommand):
# Print the model and field name
if options['verbosity']:
self.stdout.write(
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
f"{model._meta.label}.{field.target_field} ({field.name})... ",
ending='\n' if options['verbosity'] >= 2 else ''
)
self.stdout.flush()
@@ -89,23 +90,26 @@ class Command(BaseCommand):
naturalized_value = naturalize(value, max_length=field.max_length)
if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
self.stdout.write(f" {value} -> {naturalized_value}", ending='')
self.stdout.flush()
# Update each unique field value in bulk
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
if options['verbosity'] >= 2:
self.stdout.write(" ({})".format(changed))
self.stdout.write(f" ({changed})")
count += changed
# Print the total count of alterations for the field
if options['verbosity'] >= 2:
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
count, model._meta.verbose_name_plural, queryset.count()
)))
self.stdout.write(self.style.SUCCESS(
f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
))
elif options['verbosity']:
self.stdout.write(self.style.SUCCESS(str(count)))
# Invalidate cached queries
invalidate_model(model)
if options['verbosity']:
self.stdout.write(self.style.SUCCESS("Done."))

View File

@@ -162,6 +162,24 @@ class CustomField(models.Model):
def __str__(self):
return self.label or self.name.replace('_', ' ').capitalize()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache instance's original name so we can check later whether it has changed
self._name = self.name
def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
"""
for ct in self.content_types.all():
model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params)
for instance in instances:
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is

View File

@@ -140,7 +140,7 @@ class Webhook(models.Model):
ret = {}
data = render_jinja2(self.additional_headers, context)
for line in data.splitlines():
header, value = line.split(':')
header, value = line.split(':', 1)
ret[header.strip()] = value.strip()
return ret

View File

@@ -4,7 +4,8 @@ from datetime import timedelta
from cacheops.signals import cache_invalidated, cache_read
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import m2m_changed, pre_delete
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter
@@ -52,7 +53,7 @@ def _handle_changed_object(request, sender, instance, **kwargs):
# Housekeeping: 0.1% chance of clearing out expired ObjectChanges
if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1:
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
ObjectChange.objects.filter(time__lt=cutoff).delete()
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
def _handle_deleted_object(request, sender, instance, **kwargs):
@@ -85,6 +86,14 @@ def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set))
def handle_cf_renamed(instance, created, **kwargs):
"""
Handle the renaming of custom field data on objects when a CustomField is renamed.
"""
if not created and instance.name != instance._name:
instance.rename_object_data(old_name=instance._name, new_name=instance.name)
def handle_cf_deleted(instance, **kwargs):
"""
Handle the cleanup of old custom field data when a CustomField is deleted.
@@ -93,6 +102,7 @@ def handle_cf_deleted(instance, **kwargs):
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField)

View File

@@ -91,6 +91,33 @@ class CustomFieldTest(TestCase):
# Delete the custom field
cf.delete()
def test_rename_customfield(self):
obj_type = ContentType.objects.get_for_model(Site)
FIELD_DATA = 'abc'
# Create a custom field
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
cf.save()
cf.content_types.set([obj_type])
# Assign custom field data to an object
site = Site.objects.create(
name='Site 1',
slug='site-1',
custom_field_data={'field1': FIELD_DATA}
)
site.refresh_from_db()
self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
# Rename the custom field
cf.name = 'field2'
cf.save()
# Check that custom field data on the object has been updated
site.refresh_from_db()
self.assertNotIn('field1', site.custom_field_data)
self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
class CustomFieldManagerTest(TestCase):

View File

@@ -1,4 +1,6 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from drf_yasg.utils import swagger_auto_schema
@@ -162,7 +164,12 @@ class PrefixViewSet(CustomFieldModelViewSet):
# Create the new Prefix(es)
if serializer.is_valid():
serializer.save()
try:
with transaction.atomic():
created = serializer.save()
self._validate_objects(created)
except ObjectDoesNotExist:
raise PermissionDenied()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -225,7 +232,12 @@ class PrefixViewSet(CustomFieldModelViewSet):
# Create the new IP address(es)
if serializer.is_valid():
serializer.save()
try:
with transaction.atomic():
created = serializer.save()
self._validate_objects(created)
except ObjectDoesNotExist:
raise PermissionDenied()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class IPAMConfig(AppConfig):
name = "ipam"
verbose_name = "IPAM"
def ready(self):
import ipam.signals

View File

@@ -192,7 +192,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
field_name='prefix',
lookup_expr='family'
)
prefix = django_filters.CharFilter(
prefix = MultiValueCharFilter(
method='filter_prefix',
label='Prefix',
)
@@ -304,13 +304,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
return queryset.filter(qs_filter)
def filter_prefix(self, queryset, name, value):
if not value.strip():
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix=query)
except (AddrFormatError, ValueError):
return queryset.none()
query_values = []
for v in value:
try:
query_values.append(netaddr.IPNetwork(v))
except (AddrFormatError, ValueError):
pass
return queryset.filter(prefix__in=query_values)
def search_within(self, queryset, name, value):
value = value.strip()

View File

@@ -465,12 +465,14 @@ class PrefixCSVForm(CustomFieldModelCSVForm):
if data:
# Limit vlan queryset by assigned site and group
params = {
f"site__{self.fields['site'].to_field_name}": data.get('site'),
f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'),
}
self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
# Limit VLAN queryset by assigned site and/or group (if specified)
params = {}
if data.get('site'):
params[f"site__{self.fields['site'].to_field_name}"] = data.get('site')
if data.get('vlan_group'):
params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group')
if params:
self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):

View File

@@ -734,13 +734,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
})
# Enforce unique IP space (if applicable)
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
) or (
self.vrf and self.vrf.enforce_unique
)):
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates()
if duplicate_ips:
if duplicate_ips and (
self.role not in IPADDRESS_ROLES_NONUNIQUE or
any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)
):
raise ValidationError({
'address': "Duplicate IP address found in {}: {}".format(
"VRF {}".format(self.vrf) if self.vrf else "global table",

21
netbox/ipam/signals.py Normal file
View File

@@ -0,0 +1,21 @@
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from dcim.models import Device
from virtualization.models import VirtualMachine
from .models import IPAddress
@receiver(pre_delete, sender=IPAddress)
def clear_primary_ip(instance, **kwargs):
"""
When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it
was a primary IP.
"""
field_name = f'primary_ip{instance.family}'
device = Device.objects.filter(**{field_name: instance}).first()
if device:
device.save()
virtualmachine = VirtualMachine.objects.filter(**{field_name: instance}).first()
if virtualmachine:
virtualmachine.save()

View File

@@ -22,7 +22,7 @@ PREFIX_LINK = """
{% for i in record.parents|as_range %}
<i class="mdi mdi-circle-small"></i>
{% endfor %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
"""
PREFIX_ROLE_LINK = """
@@ -37,7 +37,7 @@ IPADDRESS_LINK = """
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
{% else %}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %}
@@ -50,8 +50,8 @@ IPADDRESS_ASSIGN_LINK = """
VRF_LINK = """
{% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
{% elif prefix.vrf %}
{{ prefix.vrf }}
{% elif object.vrf %}
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
{% else %}
Global
{% endif %}
@@ -109,16 +109,6 @@ VLAN_MEMBER_TAGGED = """
{% endif %}
"""
TENANT_LINK = """
{% if record.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
{% elif record.vrf.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
{% else %}
&mdash;
{% endif %}
"""
#
# VRFs
@@ -208,8 +198,8 @@ class AggregateTable(BaseTable):
prefix = tables.LinkColumn(
verbose_name='Aggregate'
)
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
tenant = tables.Column(
linkify=True
)
date_added = tables.DateColumn(
format="Y-m-d",
@@ -279,8 +269,8 @@ class PrefixTable(BaseTable):
template_code=VRF_LINK,
verbose_name='VRF'
)
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
tenant = tables.Column(
linkify=True
)
site = tables.Column(
linkify=True
@@ -347,8 +337,8 @@ class IPAddressTable(BaseTable):
default=AVAILABLE_LABEL
)
role = ChoiceFieldColumn()
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
tenant = tables.Column(
linkify=True
)
assigned_object = tables.Column(
linkify=True,
@@ -428,8 +418,11 @@ class InterfaceIPAddressTable(BaseTable):
verbose_name='VRF'
)
status = ChoiceFieldColumn()
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
tenant = tables.Column(
linkify=True
)
actions = ButtonsColumn(
model=IPAddress
)
class Meta(BaseTable.Meta):

View File

@@ -422,6 +422,11 @@ class PrefixTestCase(TestCase):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_prefix(self):
prefixes = Prefix.objects.all()[:2]
params = {'prefix': [prefixes[0].prefix, prefixes[1].prefix]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_is_pool(self):
params = {'is_pool': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -259,6 +259,18 @@ class TestIPAddress(TestCase):
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_nonrole_role(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role_nonrole(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)

View File

@@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
def get_extra_context(self, request, instance):
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
import_targets_table = tables.RouteTargetTable(
instance.import_targets.prefetch_related('tenant'),
@@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
return {
'prefix_count': prefix_count,
'ipaddress_count': ipaddress_count,
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}

View File

@@ -1,4 +1,4 @@
from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from .routers import OrderedDefaultRouter
from .serializers import BulkOperationSerializer, ValidatedModelSerializer, WritableNestedSerializer
@@ -9,7 +9,6 @@ __all__ = (
'ContentTypeField',
'OrderedDefaultRouter',
'SerializedPKRelatedField',
'TimeZoneField',
'ValidatedModelSerializer',
'WritableNestedSerializer',
)

View File

@@ -104,21 +104,6 @@ class ContentTypeField(RelatedField):
return f"{obj.app_label}.{obj.model}"
class TimeZoneField(serializers.Field):
"""
Represent a pytz time zone.
"""
def to_representation(self, obj):
return obj.zone if obj else None
def to_internal_value(self, data):
if not data:
return ""
if data not in pytz.common_timezones:
raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
return pytz.timezone(data)
class SerializedPKRelatedField(PrimaryKeyRelatedField):
"""
Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related

View File

@@ -34,6 +34,9 @@ REDIS = {
'PASSWORD': '',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
@@ -44,6 +47,9 @@ REDIS = {
'PASSWORD': '',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
@@ -154,6 +160,9 @@ LOGIN_TIMEOUT = None
# Setting this to True will display a "maintenance mode" banner at the top of every page.
MAINTENANCE_MODE = False
# The URL to use when mapping physical addresses or GPS coordinates
MAPS_URL = 'https://maps.google.com/?q='
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
# all objects by specifying "?limit=0".

View File

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
VERSION = '2.10.4'
VERSION = '2.10.10'
# Hostname
HOSTNAME = platform.node()
@@ -88,10 +88,9 @@ LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAPS_URL = getattr(configuration, 'MAPS_URL', 'https://maps.google.com/?q=')
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
@@ -118,18 +117,23 @@ SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
# Validate update repo URL and timeout
if RELEASE_CHECK_URL:
try:
URLValidator(RELEASE_CHECK_URL)
except ValidationError:
raise ImproperlyConfigured(
validator = URLValidator(
message=(
"RELEASE_CHECK_URL must be a valid API URL. Example: "
"https://api.github.com/repos/netbox-community/netbox"
)
)
try:
validator(RELEASE_CHECK_URL)
except ValidationError as err:
raise ImproperlyConfigured(str(err))
# Enforce a minimum cache timeout for update checks
if RELEASE_CHECK_TIMEOUT < 3600:
@@ -211,6 +215,7 @@ TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
# Caching
if 'caching' not in REDIS:
@@ -229,6 +234,7 @@ CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default'
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
CACHING_REDIS_SKIP_TLS_VERIFY = CACHING_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
#
@@ -391,23 +397,17 @@ if CACHING_REDIS_USING_SENTINEL:
'locations': CACHING_REDIS_SENTINELS,
'service_name': CACHING_REDIS_SENTINEL_SERVICE,
'db': CACHING_REDIS_DATABASE,
'password': CACHING_REDIS_PASSWORD,
}
else:
if CACHING_REDIS_SSL:
REDIS_CACHE_CON_STRING = 'rediss://'
else:
REDIS_CACHE_CON_STRING = 'redis://'
if CACHING_REDIS_PASSWORD:
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
REDIS_CACHE_CON_STRING,
CACHING_REDIS_HOST,
CACHING_REDIS_PORT,
CACHING_REDIS_DATABASE
)
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
CACHEOPS_REDIS = {
'host': CACHING_REDIS_HOST,
'port': CACHING_REDIS_PORT,
'db': CACHING_REDIS_DATABASE,
'password': CACHING_REDIS_PASSWORD,
'ssl': CACHING_REDIS_SSL,
'ssl_cert_reqs': None if CACHING_REDIS_SKIP_TLS_VERIFY else 'required',
}
if not CACHE_TIMEOUT:
CACHEOPS_ENABLED = False
@@ -555,6 +555,7 @@ else:
'DB': TASKS_REDIS_DATABASE,
'PASSWORD': TASKS_REDIS_PASSWORD,
'SSL': TASKS_REDIS_SSL,
'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
}

View File

@@ -792,7 +792,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
if form.cleaned_data[name]:
getattr(obj, name).set(form.cleaned_data[name])
# Normal fields
elif form.cleaned_data[name] not in (None, ''):
elif form.cleaned_data[name] not in (None, '', []):
setattr(obj, name, form.cleaned_data[name])
# Update custom fields

View File

@@ -177,6 +177,10 @@ nav ul.pagination {
margin-top: 0;
margin-bottom: 8px !important;
}
.pagination > li > a > .mdi::before {
top: 0;
font-size: 14px;
}
/* Devices */
table.component-list td.subtable {
@@ -358,9 +362,6 @@ table.report th a {
.text-nowrap {
white-space: nowrap;
}
.banner-bottom {
margin-bottom: 50px;
}
.panel table {
margin-bottom: 0;
}

View File

@@ -55,7 +55,7 @@
{% block content %}{% endblock %}
<div class="push"></div>
{% if settings.BANNER_BOTTOM %}
<div class="alert alert-info text-center banner-bottom" role="alert">
<div class="alert alert-info text-center" style="margin-bottom: 50px" role="alert">
{{ settings.BANNER_BOTTOM|safe }}
</div>
{% endif %}

View File

@@ -69,7 +69,7 @@
<h5>Total segments: {{ traced_path|length }}</h5>
<h5>Total length:
{% if total_length %}
{{ total_length|floatformat:"-2" }} Meters /
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters /
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
{% else %}
<span class="text-muted">N/A</span>

View File

@@ -153,16 +153,17 @@
</li>
{% endif %}
{% endwith %}
{% if perms.dcim.napalm_read_device %}
{% if object.status != 'active' %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='Device must be in active status' %}
{% elif not object.platform %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No platform assigned to this device' %}
{% elif not object.platform.napalm_driver %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No NAPALM driver assigned for this platform' %}
{% else %}
{% include 'dcim/inc/device_napalm_tabs.html' %}
{% endif %}
{% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
{# NAPALM-enabled tabs #}
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_status' pk=object.pk %}">Status</a>
</li>
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">LLDP Neighbors</a>
</li>
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_config' pk=object.pk %}">Configuration</a>
</li>
{% endif %}
{% if perms.extras.view_configcontext %}
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load helpers %}
{% load perms %}
{% load custom_links %}
{% load plugins %}
{% block header %}
@@ -30,6 +31,9 @@
{% endif %}
</div>
<h1>{% block title %}{{ object.device }} / {{ object }}{% endblock %}</h1>
<div class="pull-right noprint">
{% custom_links object %}
</div>
<ul class="nav nav-tabs">
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>

View File

@@ -9,8 +9,3 @@
</a>
{% endif %}
{% endif %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=cable.pk %}?return_url={{ object.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -1,15 +0,0 @@
{% if not disabled_message %}
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_status' pk=object.pk %}">Status</a>
</li>
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">LLDP Neighbors</a>
</li>
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_config' pk=object.pk %}">Configuration</a>
</li>
{% else %}
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Status</a></li>
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">LLDP Neighbors</a></li>
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Configuration</a></li>
{% endif %}

View File

@@ -1,6 +1,7 @@
{% extends 'dcim/device_component.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
@@ -226,7 +227,23 @@
</div>
<div class="row">
<div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">

View File

@@ -137,7 +137,7 @@
<td>
{% if object.physical_address %}
<div class="pull-right noprint">
<a href="https://maps.google.com/?q={{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
<a href="{{ settings.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
<i class="mdi mdi-map-marker"></i> Map it
</a>
</div>
@@ -156,7 +156,7 @@
<td>
{% if object.latitude and object.longitude %}
<div class="pull-right noprint">
<a href="https://maps.google.com/?q={{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
<a href="{{ settings.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
<i class="mdi mdi-map-marker"></i> Map it
</a>
</div>

View File

@@ -66,9 +66,11 @@
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% else %}
<span class="muted">&mdash;</span>
{% endif %}
</td>
<td>{{ message }}</td>
<td class="rendered-markdown">{{ message|render_markdown }}</td>
</tr>
{% endfor %}
{% endfor %}

View File

@@ -16,7 +16,7 @@
</div>
</div>
<h1>{{ script }}</h1>
<p>{{ script.Meta.description }}</p>
<p>{{ script.Meta.description|render_markdown }}</p>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#run" role="tab" data-toggle="tab" class="active">Run</a>

View File

@@ -26,7 +26,7 @@
<td>
{% include 'extras/inc/job_label.html' with result=script.result %}
</td>
<td>{{ script.Meta.description }}</td>
<td>{{ script.Meta.description|render_markdown }}</td>
{% if script.result %}
<td class="text-right">
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created }}</a>

View File

@@ -18,7 +18,7 @@
</div>
</div>
<h1>{{ script }}</h1>
<p>{{ script.Meta.description }}</p>
<p>{{ script.Meta.description|render_markdown }}</p>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#log" role="tab" data-toggle="tab" class="active">Log</a>
@@ -110,4 +110,4 @@ function jobTerminatedAction(){
</script>
<script src="{% static 'js/job_result.js' %}?v{{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=js/job_result.js'"></script>
{% endblock %}
{% endblock %}

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load buttons %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load static %}
{% block content %}
@@ -28,54 +29,56 @@
{% block sidebar %}{% endblock %}
</div>
{% endif %}
<div class="table-responsive">
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
{% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{% if table.paginator.num_pages > 1 %}
<div id="select_all_box" class="hidden panel panel-default noprint">
<div class="panel-body">
<div class="checkbox-inline">
<label for="select_all">
<input type="checkbox" id="select_all" name="_all" />
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
</button>
{% endif %}
{% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{% if table.paginator.num_pages > 1 %}
<div id="select_all_box" class="hidden panel panel-default noprint">
<div class="panel-body">
<div class="checkbox-inline">
<label for="select_all">
<input type="checkbox" id="select_all" name="_all" />
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% render_table table 'inc/table.html' %}
<div class="pull-left noprint">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
{% endif %}
{% include table_template|default:'responsive_table.html' %}
<div class="pull-left noprint">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
</form>
{% else %}
{% include table_template|default:'responsive_table.html' %}
{% endif %}
</form>
{% else %}
{% render_table table 'inc/table.html' %}
{% endif %}
{% endwith %}
</div>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
<div class="clearfix"></div>
</div>

View File

@@ -304,13 +304,7 @@
{% for change in changelog %}
{% with action=change.get_action_display|lower %}
<div class="list-group-item">
{% if action == 'created' %}
<span class="label label-success">Created</span>
{% elif action == 'updated' %}
<span class="label label-warning">Modified</span>
{% elif action == 'deleted' %}
<span class="label label-danger">Deleted</span>
{% endif %}
<span class="label label-{{ change.get_action_class }}">{{ change.get_action_display }}</span>
{{ change.changed_object_type.name|bettertitle }}
{% if change.changed_object.get_absolute_url %}
<a href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>

View File

@@ -95,6 +95,12 @@
<td>
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
</td>
</tr>
<tr>
<td>IP Addresses</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
</td>
</tr>
</table>
</div>

View File

@@ -11,12 +11,8 @@
<div class="panel-heading">
<div class="pull-right noprint">
<a class="btn btn-xs btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
{% if perms.users.change_token %}
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
{% endif %}
{% if perms.users.delete_token %}
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
{% endif %}
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
</div>
<i class="mdi mdi-key"></i>
<samp><span id="token_{{ token.pk }}">{{ token.key }}</span></samp>
@@ -55,16 +51,10 @@
{% empty %}
<p>You do not have any API tokens.</p>
{% endfor %}
{% if perms.users.add_token %}
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add a token
</a>
{% else %}
<div class="alert alert-info text-center" role="alert">
You do not have permission to create new API tokens. If needed, ask an administrator to enable token creation for your account or an assigned group.
</div>
{% endif %}
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add a token
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block header %}
<div class="row noprint">
@@ -93,7 +94,23 @@
</div>
<div class="row">
<div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">

View File

@@ -24,7 +24,7 @@ class TenantGroupSerializer(ValidatedModelSerializer):
class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
group = NestedTenantGroupSerializer(required=False)
group = NestedTenantGroupSerializer(required=False, allow_null=True)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
ipaddress_count = serializers.IntegerField(read_only=True)

View File

@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
@@ -74,6 +75,15 @@ class TenantGroup(MPTTModel, ChangeLoggedModel):
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
def clean(self):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Tenant(ChangeLoggedModel, CustomFieldModel):

View File

@@ -56,6 +56,7 @@ class TenantTest(APIViewTestCases.APIViewTestCase):
model = Tenant
brief_fields = ['id', 'name', 'slug', 'url']
bulk_update_data = {
'group': None,
'description': 'New description',
}

View File

@@ -223,7 +223,7 @@ class ObjectTypeListFilter(admin.SimpleListFilter):
parameter_name = 'object_type'
def lookups(self, request, model_admin):
object_types = ObjectPermission.objects.values_list('id', flat=True).distinct()
object_types = ObjectPermission.objects.values_list('object_types__pk', flat=True).distinct()
content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
return [
(ct.pk, ct) for ct in content_types

View File

@@ -6,7 +6,7 @@ from django.contrib.auth import login as auth_login, logout as auth_logout, upda
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -282,13 +282,9 @@ class TokenEditView(LoginRequiredMixin, View):
def get(self, request, pk=None):
if pk is not None:
if not request.user.has_perm('users.change_token'):
return HttpResponseForbidden()
if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
else:
if not request.user.has_perm('users.add_token'):
return HttpResponseForbidden()
token = Token(user=request.user)
form = TokenForm(instance=token)
@@ -302,11 +298,11 @@ class TokenEditView(LoginRequiredMixin, View):
def post(self, request, pk=None):
if pk is not None:
if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
form = TokenForm(request.POST, instance=token)
else:
token = Token()
token = Token(user=request.user)
form = TokenForm(request.POST)
if form.is_valid():
@@ -314,7 +310,7 @@ class TokenEditView(LoginRequiredMixin, View):
token.user = request.user
token.save()
msg = "Modified token {}".format(token) if pk else "Created token {}".format(token)
msg = f"Modified token {token}" if pk else f"Created token {token}"
messages.success(request, msg)
if '_addanother' in request.POST:

View File

@@ -5,6 +5,7 @@ from io import StringIO
import django_filters
from django import forms
from django.conf import settings
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Count
@@ -214,7 +215,7 @@ class CommentField(forms.CharField):
default_label = ''
# TODO: Port Markdown cheat sheet to internal documentation
default_helptext = '<i class="mdi mdi-information-outline"></i> '\
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank" tabindex="-1">'\
'Markdown</a> syntax is supported'
def __init__(self, *args, **kwargs):
@@ -355,7 +356,15 @@ class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField):
Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be
rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget.
"""
pass
def clean(self, value):
"""
When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the
string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType.
"""
if self.null_option is not None and value == settings.FILTERS_NULL_CHOICE_VALUE:
return None
return super().clean(value)
class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField):

View File

@@ -114,7 +114,10 @@ class ContentTypeSelect(StaticSelect2):
class NumericArrayField(SimpleArrayField):
def to_python(self, value):
value = ','.join([str(n) for n in parse_numeric_range(value)])
if not value:
return []
if isinstance(value, str):
value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value)

View File

@@ -224,12 +224,12 @@ def prepare_cloned_fields(instance):
field = instance._meta.get_field(field_name)
field_value = field.value_from_object(instance)
# Swap out False with URL-friendly value
# Pass False as null for boolean fields
if field_value is False:
field_value = ''
params.append((field_name, ''))
# Omit empty values
if field_value not in (None, ''):
elif field_value not in (None, ''):
params.append((field_name, field_value))
# Copy tags

View File

@@ -109,12 +109,13 @@ class VMInterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
required=False,
many=True
)
count_ipaddresses = serializers.IntegerField(read_only=True)
class Meta:
model = VMInterface
fields = [
'id', 'url', 'virtual_machine', 'name', 'enabled', 'mtu', 'mac_address', 'description', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags',
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
]
def validate(self, data):

View File

@@ -80,7 +80,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
class VMInterfaceViewSet(ModelViewSet):
queryset = VMInterface.objects.prefetch_related(
'virtual_machine', 'tags', 'tagged_vlans'
'virtual_machine', 'tags', 'tagged_vlans', 'ip_addresses',
)
serializer_class = serializers.VMInterfaceSerializer
filterset_class = filters.VMInterfaceFilterSet

View File

@@ -756,6 +756,26 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
# Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
else:
# See 5643
if 'pk' in self.initial:
site = None
interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related(
'virtual_machine__cluster__site'
)
# Check interface sites. First interface should set site, further interfaces will either continue the
# loop or reset back to no site and break the loop.
for interface in interfaces:
if site is None:
site = interface.virtual_machine.cluster.site
elif interface.virtual_machine.cluster.site is not site:
site = None
break
if site is not None:
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
class VMInterfaceBulkRenameForm(BulkRenameForm):
@@ -765,7 +785,7 @@ class VMInterfaceBulkRenameForm(BulkRenameForm):
)
class VMInterfaceFilterForm(forms.Form):
class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
model = VMInterface
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
@@ -786,6 +806,10 @@ class VMInterfaceFilterForm(forms.Form):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
mac_address = forms.CharField(
required=False,
label='MAC address'
)
tag = TagFilterField(model)

View File

@@ -1,5 +1,5 @@
import django_tables2 as tables
from django.conf import settings
from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import COL_TENANT
from utilities.tables import (
@@ -125,10 +125,18 @@ class VirtualMachineDetailTable(VirtualMachineTable):
linkify=True,
verbose_name='IPv6 Address'
)
primary_ip = tables.Column(
linkify=True,
verbose_name='IP Address'
)
if settings.PREFER_IPV4:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
else:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
tags = TagColumn(
url_name='virtualization:virtualmachine_list'
)

View File

@@ -1,23 +1,23 @@
Django==3.1.3
Django==3.1.8
django-cacheops==5.1
django-cors-headers==3.5.0
django-debug-toolbar==3.1.1
django-cors-headers==3.7.0
django-debug-toolbar==3.2
django-filter==2.4.0
django-mptt==0.11.0
django-mptt==0.12.0
django-pglocks==1.0.4
django-prometheus==2.1.0
django-rq==2.4.0
django-tables2==2.3.3
django-rq==2.4.1
django-tables2==2.3.4
django-taggit==1.3.0
django-timezone-field==4.0
djangorestframework==3.12.2
django-timezone-field==4.1.2
djangorestframework==3.12.4
drf-yasg[validation]==1.20.0
gunicorn==20.0.4
Jinja2==2.11.2
Markdown==3.3.3
gunicorn==20.1.0
Jinja2==2.11.3
Markdown==3.3.4
netaddr==0.8.0
Pillow==8.0.1
Pillow==8.2.0
psycopg2-binary==2.8.6
pycryptodome==3.9.9
PyYAML==5.3.1
svgwrite==1.4
pycryptodome==3.10.1
PyYAML==5.4.1
svgwrite==1.4.1

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