Compare commits

...

103 Commits

Author SHA1 Message Date
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
Jeremy Stretch
856d2e3176 Merge pull request #5694 from netbox-community/develop
Release v2.10.4
2021-01-26 16:01:00 -05:00
Daniel Sheppard
3409a1bfba Merge remote-tracking branch 'origin/develop' into develop 2021-01-26 14:04:22 -06:00
Daniel Sheppard
fc8f02c180 Corrects error with ListSerializer as request_body 2021-01-26 14:03:46 -06:00
Jeremy Stretch
03e48161a1 Release v2.10.4 2021-01-26 13:06:29 -05:00
Jeremy Stretch
def63329f0 Merge pull request #5693 from aaroneg/patch-1
update python package name
2021-01-26 13:03:11 -05:00
Aaron
0680b01a96 update python package name
At least on ubuntu 20.04, the python3 package is now 3.8, but the package 'python3' points to the current best version of python available without needing to specialize a minor version and should require fewer changes to the document.
2021-01-26 11:47:33 -06:00
Daniel Sheppard
592e788a7d Merge branch 'develop' of https://github.com/netbox-community/netbox into develop 2021-01-26 10:42:45 -06:00
Daniel Sheppard
aabc1a8265 Fixes: #5232 - Corrects swagger definition 2021-01-26 10:42:01 -06:00
Daniel Sheppard
a23ff4e519 Fixes: #5232 - Corrects swagger definition 2021-01-26 10:34:07 -06:00
Jeremy Stretch
edc015d9bf Emphasize use of GitHub discussions in README 2021-01-26 11:20:06 -05:00
Jeremy Stretch
d5a0e12283 Certain component types are optional 2021-01-26 10:35:03 -05:00
Jeremy Stretch
90e8f26cd4 Closes #5678: Show available type choices for all device component import forms 2021-01-26 10:17:58 -05:00
Jeremy Stretch
d4e83ca1c0 Fixes #5683: Correct rack elevation displayed when viewing a reservation 2021-01-26 09:57:33 -05:00
Jeremy Stretch
137aa9da2c Fixes #5648: Include VC member interfaces on interfaces tab count when viewing VC master 2021-01-25 14:29:03 -05:00
Jeremy Stretch
87c600aa7c Fixes #5665: Validate rack group is assigned to same site when creating a rack 2021-01-25 14:19:32 -05:00
Jeremy Stretch
08dfe64301 Merge pull request #5677 from Alef-Burzmali/develop
Fix how SECRET_KEY is generated
2021-01-25 13:55:45 -05:00
Jeremy Stretch
0e48ee5f9e Merge pull request #5675 from rileyL6122428/patch-1
Fixes small typos in Configuration > Required Settings and Configuration > Optional Settings
2021-01-24 18:07:41 -05:00
Thomas Fargeix
b6e532f01d Fix how SECRET_KEY is generated
Use secrets.choice instead of random.sample to generate the secret key.
2021-01-24 21:20:55 +01:00
Riley Littlefield
60baa5e59e Fixes small typo in optional settings 2021-01-23 14:06:48 -05:00
Riley Littlefield
9eb64dc6a4 Fixes another typo 2021-01-23 13:54:44 -05:00
Riley Littlefield
3de04094fb Fixes typo 2021-01-23 13:41:48 -05:00
Jeremy Stretch
5e962719ca Closes #5542: Show cable trace lengths in both meters and feet 2021-01-20 21:29:23 -05:00
Jeremy Stretch
fefc623343 Changelog for #5603 2021-01-20 20:48:24 -05:00
Jeremy Stretch
5c40081d84 Merge pull request #5662 from cpmills1975/5603-white-cable-fix
Fix white cables
2021-01-20 20:47:28 -05:00
Chris Mills
e739d6aa05 Fix white cables 2021-01-20 23:52:54 +00:00
Jeremy Stretch
0994719b91 Add NetBox installation video to docs 2021-01-20 15:36:04 -05:00
Jeremy Stretch
f469920759 Fixes #5640: Fix permissions assessment when adding VM interfaces in bulk 2021-01-20 15:18:13 -05:00
Jeremy Stretch
3c9be8cd08 Fixes #5639: Fix filtering connection lists by device name 2021-01-19 11:24:34 -05:00
Jeremy Stretch
a0e82e1817 Fixes #5574: Restrict the creation of device bay templates on non-parent device types 2021-01-19 10:49:56 -05:00
Jeremy Stretch
69bf451b20 Changelog for #5586 and #5612 2021-01-19 09:41:49 -05:00
Jeremy Stretch
58699a220b Merge pull request #5596 from FragmentedPacket/5586-vc-filtering
Closes 5586: Adds name, master, and master_id filtering
2021-01-19 09:31:24 -05:00
Jeremy Stretch
3f70f685bb Merge pull request #5634 from phoerious/5612-gg45-tera-connector
Add choices for GG45 and TERA connectors and Cat7a/Cat8 cables
2021-01-19 09:28:33 -05:00
Janek Bevendorff
d838a76461 Add choices for GG45 and TERA connectors and Cat7a/Cat8 cables
Fixes #5612
2021-01-18 14:13:07 +01:00
Jeremy Stretch
e13d96a6f2 Don't pin Ubuntu installations to Python 3.6 2021-01-17 14:08:59 -05:00
Jeremy Stretch
1e1e2d5f54 Fixes #5597: Fix ordering devices by primary IP address 2021-01-11 11:28:03 -05:00
Mikhail Yohman
c51d2a56ac Closes 5586: Adds name, master, and master_id filtering 2021-01-08 20:18:48 -07:00
Jeremy Stretch
e9d888bf63 Closes #5570: Add "management only" filter widget for interfaces list 2021-01-07 11:29:59 -05:00
Jeremy Stretch
47b7ec8d00 Fixes #5584: Restore power utilization panel under device view 2021-01-07 11:19:11 -05:00
Jeremy Stretch
c8f09f28b1 PRVB 2021-01-05 21:10:58 -05:00
69 changed files with 677 additions and 314 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

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

@@ -0,0 +1,63 @@
---
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
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

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,38 @@
---
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: checkboxes
attributes:
label: Area
description: To what section(s) of the documentation does this change pertain?
options:
- label: Installation instructions
- label: Configuration parameters
- label: Functionality/features
- label: REST API
- label: Administration/development
- label: Other
- type: textarea
attributes:
label: Proposed Changes
description: "Describe the proposed changes and why they are necessary"
validations:
required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

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,58 @@
---
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.)"
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

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,27 @@
---
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
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

3
.github/stale.yml vendored
View File

@@ -1,8 +1,5 @@
# 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

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://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ).
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,8 +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? Please start a [discussion on GitHub](https://github.com/netbox-community/netbox/discussions),
or join us in the **#netbox** Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
### 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://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - 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

@@ -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

@@ -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

@@ -56,7 +56,7 @@ BASE_PATH = 'netbox/'
Default: 900
The number of seconds to cache entries will be retained before expiring.
The number of seconds that cache entries will be retained before expiring.
---

View File

@@ -5,7 +5,7 @@
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attackes](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
!!! note
This parameter must always be defined as a list or tuple, even if only value is provided.
This parameter must always be defined as a list or tuple, even if only a single value is provided.
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
@@ -101,7 +101,7 @@ REDIS = {
If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal
configuration necessary to convert NetBox to recognize it. It requires the removal of the `HOST` and `PORT` keys from
above and the addition of two new keys.
above and the addition of three new keys.
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
of the Redis server and port for each sentinel instance to connect to

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://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - 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

@@ -7,12 +7,12 @@ This section of the documentation discusses installing and configuring the NetBo
Begin by installing all system packages required by NetBox and its dependencies.
!!! note
NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8. This documentation assumes Python 3.6.
NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8.
### Ubuntu
```no-highlight
sudo apt install -y python3.6 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
```
### CentOS

View File

@@ -140,7 +140,7 @@ 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`:

View File

@@ -11,6 +11,10 @@ The following sections detail how to set up a new instance of NetBox:
5. [HTTP server](5-http-server.md)
6. [LDAP authentication](6-ldap.md) (optional)
The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04 for your reference.
<iframe width="560" height="315" src="https://www.youtube.com/embed/dFANGlxXEng" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Requirements
| Dependency | Minimum Version |

View File

@@ -1,5 +1,65 @@
# NetBox v2.10
## 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
* [#5542](https://github.com/netbox-community/netbox/issues/5542) - Show cable trace lengths in both meters and feet
* [#5570](https://github.com/netbox-community/netbox/issues/5570) - Add "management only" filter widget for interfaces list
* [#5586](https://github.com/netbox-community/netbox/issues/5586) - Allow filtering virtual chassis by name and master
* [#5612](https://github.com/netbox-community/netbox/issues/5612) - Add GG45 and TERA port types, and CAT7a and CAT8 cable types
* [#5678](https://github.com/netbox-community/netbox/issues/5678) - Show available type choices for all device component import forms
### Bug Fixes
* [#5232](https://github.com/netbox-community/netbox/issues/5232) - Correct swagger definition for ip_prefixes_available-ips_create API
* [#5574](https://github.com/netbox-community/netbox/issues/5574) - Restrict the creation of device bay templates on non-parent device types
* [#5584](https://github.com/netbox-community/netbox/issues/5584) - Restore power utilization panel under device view
* [#5597](https://github.com/netbox-community/netbox/issues/5597) - Fix ordering devices by primary IP address
* [#5603](https://github.com/netbox-community/netbox/issues/5603) - Fix display of white cables in trace view
* [#5639](https://github.com/netbox-community/netbox/issues/5639) - Fix filtering connection lists by device name
* [#5640](https://github.com/netbox-community/netbox/issues/5640) - Fix permissions assessment when adding VM interfaces in bulk
* [#5648](https://github.com/netbox-community/netbox/issues/5648) - Include VC member interfaces on interfaces tab count when viewing VC master
* [#5665](https://github.com/netbox-community/netbox/issues/5665) - Validate rack group is assigned to same site when creating a rack
* [#5683](https://github.com/netbox-community/netbox/issues/5683) - Correct rack elevation displayed when viewing a reservation
---
## v2.10.3 (2021-01-05)
### Bug Fixes

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

@@ -686,6 +686,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
@@ -801,6 +802,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)'),
)
),
@@ -873,6 +875,10 @@ class PortTypeChoices(ChoiceSet):
TYPE_8P6C = '8p6c'
TYPE_8P4C = '8p4c'
TYPE_8P2C = '8p2c'
TYPE_GG45 = 'gg45'
TYPE_TERA4P = 'tera-4p'
TYPE_TERA2P = 'tera-2p'
TYPE_TERA1P = 'tera-1p'
TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc'
TYPE_MRJ21 = 'mrj21'
@@ -898,6 +904,10 @@ class PortTypeChoices(ChoiceSet):
(TYPE_8P6C, '8P6C'),
(TYPE_8P4C, '8P4C'),
(TYPE_8P2C, '8P2C'),
(TYPE_GG45, 'GG45'),
(TYPE_TERA4P, 'TERA 4P'),
(TYPE_TERA2P, 'TERA 2P'),
(TYPE_TERA1P, 'TERA 1P'),
(TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'),
(TYPE_MRJ21, 'MRJ21'),
@@ -936,6 +946,8 @@ class CableTypeChoices(ChoiceSet):
TYPE_CAT6 = 'cat6'
TYPE_CAT6A = 'cat6a'
TYPE_CAT7 = 'cat7'
TYPE_CAT7A = 'cat7a'
TYPE_CAT8 = 'cat8'
TYPE_DAC_ACTIVE = 'dac-active'
TYPE_DAC_PASSIVE = 'dac-passive'
TYPE_MRJ21_TRUNK = 'mrj21-trunk'
@@ -960,6 +972,8 @@ class CableTypeChoices(ChoiceSet):
(TYPE_CAT6, 'CAT6'),
(TYPE_CAT6A, 'CAT6a'),
(TYPE_CAT7, 'CAT7'),
(TYPE_CAT7A, 'CAT7a'),
(TYPE_CAT8, 'CAT8'),
(TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
(TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'),

View File

@@ -264,7 +264,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
)
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -1016,6 +1016,16 @@ class VirtualChassisFilterSet(BaseFilterSet):
method='search',
label='Search',
)
master_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Master (ID)',
)
master = django_filters.ModelMultipleChoiceFilter(
field_name='master__name',
queryset=Device.objects.all(),
to_field_name='name',
label='Master (name)',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='master__site__region',
@@ -1055,7 +1065,7 @@ class VirtualChassisFilterSet(BaseFilterSet):
class Meta:
model = VirtualChassis
fields = ['id', 'domain']
fields = ['id', 'domain', 'name']
def search(self, queryset, name, value):
if not value.strip():
@@ -1142,7 +1152,7 @@ class ConnectionFilterSet:
def filter_device(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(device_id__in=value)
return queryset.filter(**{f'{name}__in': value})
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):

View File

@@ -2352,6 +2352,11 @@ class ConsolePortCSVForm(CSVModelForm):
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
choices=ConsolePortTypeChoices,
required=False,
help_text='Port type'
)
class Meta:
model = ConsolePort
@@ -2425,6 +2430,11 @@ class ConsoleServerPortCSVForm(CSVModelForm):
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
choices=ConsolePortTypeChoices,
required=False,
help_text='Port type'
)
class Meta:
model = ConsoleServerPort
@@ -2510,6 +2520,11 @@ class PowerPortCSVForm(CSVModelForm):
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
choices=PowerPortTypeChoices,
required=False,
help_text='Port type'
)
class Meta:
model = PowerPort
@@ -2630,6 +2645,11 @@ class PowerOutletCSVForm(CSVModelForm):
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
choices=PowerOutletTypeChoices,
required=False,
help_text='Outlet type'
)
power_port = CSVModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
@@ -2687,6 +2707,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
mgmt_only = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
mac_address = forms.CharField(
required=False,
label='MAC address'

View File

@@ -363,3 +363,9 @@ class DeviceBayTemplate(ComponentTemplateModel):
name=self.name,
label=self.label
)
def clean(self):
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
raise ValidationError(
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
)

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.
@@ -479,7 +479,7 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
@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.
@@ -624,7 +624,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
# 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

@@ -299,6 +299,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def clean(self):
super().clean()
# Validate group/site assignment
if self.site and self.group and self.group.site != self.site:
raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).")
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError("Must specify a unit when setting an outer width/depth")

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,10 +128,18 @@ class DeviceTable(BaseTable):
verbose_name='Type',
text=lambda record: record.device_type.display_name
)
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'
)
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'
@@ -406,6 +415,7 @@ class BaseInterfaceTable(BaseTable):
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
mgmt_only = BooleanColumn()
tags = TagColumn(
url_name='dcim:interface_list'
)

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

@@ -740,7 +740,10 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
manufacturer=manufacturer,
model='Device Type 1',
slug='device-type-1',
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
)
device_bay_templates = (

View File

@@ -2399,9 +2399,9 @@ class VirtualChassisTestCase(TestCase):
Device.objects.bulk_create(devices)
virtual_chassis = (
VirtualChassis(master=devices[0], domain='Domain 1'),
VirtualChassis(master=devices[2], domain='Domain 2'),
VirtualChassis(master=devices[4], domain='Domain 3'),
VirtualChassis(name='VC 1', master=devices[0], domain='Domain 1'),
VirtualChassis(name='VC 2', master=devices[2], domain='Domain 2'),
VirtualChassis(name='VC 3', master=devices[4], domain='Domain 3'),
)
VirtualChassis.objects.bulk_create(virtual_chassis)
@@ -2417,6 +2417,17 @@ class VirtualChassisTestCase(TestCase):
params = {'domain': ['Domain 1', 'Domain 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_master(self):
masters = Device.objects.all()
params = {'master_id': [masters[0].pk, masters[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'master': [masters[0].name, masters[2].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['VC 1', 'VC 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}

View File

@@ -396,6 +396,7 @@ manufacturer: Generic
model: TEST-1000
slug: test-1000
u_height: 2
subdevice_role: parent
comments: test comment
console-ports:
- name: Console Port 1
@@ -831,8 +832,8 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
)
DeviceType.objects.bulk_create(devicetypes)

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

@@ -1,7 +1,6 @@
#!/usr/bin/env python
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
import random
import secrets
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
secure_random = random.SystemRandom()
print(''.join(secure_random.sample(charset, 50)))
print(''.join(secrets.choice(charset) for _ in range(50)))

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)
@@ -178,7 +185,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
@swagger_auto_schema(method='get', responses={200: serializers.AvailableIPSerializer(many=True)})
@swagger_auto_schema(method='post', responses={201: serializers.AvailableIPSerializer(many=True)},
request_body=serializers.AvailableIPSerializer(many=False))
request_body=serializers.AvailableIPSerializer(many=True))
@action(detail=True, url_path='available-ips', methods=['get', 'post'], queryset=IPAddress.objects.all())
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def available_ips(self, request, pk=None):
@@ -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

@@ -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

@@ -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",

View File

@@ -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 %}
@@ -114,6 +114,8 @@ TENANT_LINK = """
<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>*
{% elif object.tenant %}
<a href="{% url 'tenancy:tenant' slug=object.tenant.slug %}" title="{{ object.tenant.description }}">{{ object.tenant }}</a>
{% else %}
&mdash;
{% endif %}
@@ -431,6 +433,9 @@ class InterfaceIPAddressTable(BaseTable):
tenant = tables.TemplateColumn(
template_code=TENANT_LINK
)
actions = ButtonsColumn(
model=IPAddress
)
class Meta(BaseTable.Meta):
model = IPAddress

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

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup
#
VERSION = '2.10.3'
VERSION = '2.10.6'
# Hostname
HOSTNAME = platform.node()
@@ -391,6 +391,7 @@ 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:

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 {

View File

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

View File

@@ -204,7 +204,7 @@
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% if power_ports and poweroutlets %}
{% if object.powerports.exists and object.poweroutlets.exists %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Power Utilization</strong>
@@ -217,10 +217,10 @@
<th>Available</th>
<th>Utilization</th>
</tr>
{% for pp in power_ports %}
{% with utilization=pp.get_power_draw powerfeed=pp.connected_endpoint %}
{% for powerport in object.powerports.all %}
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoint %}
<tr>
<td>{{ pp }}</td>
<td>{{ powerport }}</td>
<td>{{ utilization.outlet_count }}</td>
<td>{{ utilization.allocated }}VA</td>
{% if powerfeed.available_power %}

View File

@@ -90,7 +90,7 @@
<li role="presentation" {% if active_tab == 'device' %} class="active"{% endif %}>
<a href="{% url 'dcim:device' pk=object.pk %}">Device</a>
</li>
{% with interface_count=object.interfaces.count %}
{% with interface_count=object.vc_interfaces.count %}
{% if interface_count %}
<li role="presentation" {% if active_tab == 'interfaces' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>

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,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

@@ -127,22 +127,20 @@
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% with rack=object.rack %}
<div class="row" style="margin-bottom: 20px">
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Front</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Rear</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
<div class="row" style="margin-bottom: 20px">
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Front</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
</div>
{% endwith %}
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Rear</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
</div>
</div>
{% plugin_right_page object %}
</div>
</div>

View File

@@ -1,6 +1,6 @@
{% load helpers %}
<div class="cable" style="border-left-color: #{{ cable.color|default:'606060' }}; {% if cable.status != 'connected' %} border-left-style: dashed{% endif %}">
<div class="cable" style="border-left-color: #{% if cable.color == 'ffffff' %}909090; border-left-style: double; border-left-width: 6px;{% else %}{{ cable.color|default:'606060' }};{% endif %} {% if cable.status != 'connected' %} border-left-style: dashed{% endif %}">
<strong>
<a href="{% url 'dcim:cable' pk=cable.pk %}">
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}

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

@@ -7,7 +7,7 @@
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.virtualization.add_vminterface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
</ul>
</div>
{% endif %}

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

@@ -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

@@ -28,29 +28,38 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
serializer = super().get_request_serializer()
if serializer is not None and self.method in self.implicit_body_methods:
properties = {}
for child_name, child in serializer.fields.items():
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
properties[child_name] = None
if properties:
if type(serializer) not in self.writable_serializers:
writable_name = 'Writable' + type(serializer).__name__
meta_class = getattr(type(serializer), 'Meta', None)
if meta_class:
ref_name = 'Writable' + get_serializer_ref_name(serializer)
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
properties['Meta'] = writable_meta
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
writable_class = self.writable_serializers[type(serializer)]
serializer = writable_class()
writable_class = self.get_writable_class(serializer)
if writable_class is not None:
if hasattr(serializer, 'child'):
child_serializer = self.get_writable_class(serializer.child)
serializer = writable_class(child=child_serializer)
else:
serializer = writable_class()
return serializer
def get_writable_class(self, serializer):
properties = {}
fields = {} if hasattr(serializer, 'child') else serializer.fields
for child_name, child in fields.items():
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
properties[child_name] = None
if properties:
if type(serializer) not in self.writable_serializers:
writable_name = 'Writable' + type(serializer).__name__
meta_class = getattr(type(serializer), 'Meta', None)
if meta_class:
ref_name = 'Writable' + get_serializer_ref_name(serializer)
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
properties['Meta'] = writable_meta
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
writable_class = self.writable_serializers[type(serializer)]
return writable_class
class SerializedPKRelatedFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):

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
@@ -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

@@ -220,6 +220,14 @@ def as_range(n):
return range(n)
@register.filter()
def meters_to_feet(n):
"""
Convert a length from meters to feet.
"""
return float(n) * 3.28084
#
# Tags
#

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

@@ -396,3 +396,6 @@ class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
model_form = forms.VMInterfaceForm
filterset = filters.VirtualMachineFilterSet
table = tables.VirtualMachineTable
def get_required_permission(self):
return f'virtualization.add_vminterface'

View File

@@ -1,23 +1,23 @@
Django==3.1.3
Django==3.1.7
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-tables2==2.3.4
django-taggit==1.3.0
django-timezone-field==4.0
django-timezone-field==4.1.1
djangorestframework==3.12.2
drf-yasg[validation]==1.20.0
gunicorn==20.0.4
Jinja2==2.11.2
Markdown==3.3.3
Jinja2==2.11.3
Markdown==3.3.4
netaddr==0.8.0
Pillow==8.0.1
Pillow==8.1.2
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

View File

@@ -29,19 +29,25 @@ eval $COMMAND || {
# Activate the virtual environment
source "${VIRTUALENV}/bin/activate"
# Upgrade pip
COMMAND="pip install --upgrade pip"
echo "Updating pip ($COMMAND)..."
eval $COMMAND || exit 1
pip -V
# Install necessary system packages
COMMAND="pip3 install wheel"
COMMAND="pip install wheel"
echo "Installing Python system packages ($COMMAND)..."
eval $COMMAND || exit 1
# Install required Python packages
COMMAND="pip3 install -r requirements.txt"
COMMAND="pip install -r requirements.txt"
echo "Installing core dependencies ($COMMAND)..."
eval $COMMAND || exit 1
# Install optional packages (if any)
if [ -s "local_requirements.txt" ]; then
COMMAND="pip3 install -r local_requirements.txt"
COMMAND="pip install -r local_requirements.txt"
echo "Installing local dependencies ($COMMAND)..."
eval $COMMAND || exit 1
elif [ -f "local_requirements.txt" ]; then