mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Merge upstream v2.10.6 into develop
Merge branch 'master' into develop
This commit is contained in:
commit
6ff4bb4b23
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: [jeremystretch]
|
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
63
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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.
|
28
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
28
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
@ -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
|
|
38
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
Normal 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.
|
54
.github/ISSUE_TEMPLATE/feature_request.md
vendored
54
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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
|
|
58
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal 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.
|
16
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
16
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
@ -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
|
|
27
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
Normal 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
3
.github/stale.yml
vendored
@ -1,8 +1,5 @@
|
|||||||
# Configuration for Stale (https://github.com/apps/stale)
|
# 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
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 45
|
daysUntilStale: 45
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ discussions.
|
|||||||
|
|
||||||
### Slack
|
### 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
|
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
|
history, so try to avoid it for any discussions would benefit from being
|
||||||
preserved for future reference.
|
preserved for future reference.
|
||||||
@ -185,11 +185,5 @@ overlooked.
|
|||||||
sync to review agenda items. This meeting provides opportunity to present and
|
sync to review agenda items. This meeting provides opportunity to present and
|
||||||
discuss pressing topics. Meetings are held as virtual audio/video conferences.
|
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
|
* Maintainers with no substantial recorded activity in a 60-day period will be
|
||||||
removed from the project.
|
removed from the project.
|
||||||
|
@ -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/).
|
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),
|
### Discussion
|
||||||
or join us in the **#netbox** Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
|
|
||||||
|
* [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
|
### Build Status
|
||||||
|
|
||||||
|
@ -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.
|
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
|
!!! 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).
|
||||||
|
@ -66,7 +66,7 @@ class DeviceConnectionsReport(Report):
|
|||||||
for power_port in PowerPort.objects.filter(device=device):
|
for power_port in PowerPort.objects.filter(device=device):
|
||||||
if power_port.connected_endpoint is not None:
|
if power_port.connected_endpoint is not None:
|
||||||
connected_ports += 1
|
connected_ports += 1
|
||||||
if not power_port.connection_status:
|
if not power_port.path.is_active:
|
||||||
self.log_warning(
|
self.log_warning(
|
||||||
device,
|
device,
|
||||||
"Power connection for {} marked as planned".format(power_port.name)
|
"Power connection for {} marked as planned".format(power_port.name)
|
||||||
|
@ -68,7 +68,7 @@ If no body template is specified, the request body will be populated with a JSON
|
|||||||
|
|
||||||
## Webhook Processing
|
## 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.
|
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.
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ BASE_PATH = 'netbox/'
|
|||||||
|
|
||||||
Default: 900
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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).
|
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
|
!!! 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)).
|
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
|
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
|
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
|
* `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
|
of the Redis server and port for each sentinel instance to connect to
|
||||||
|
@ -4,12 +4,12 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
|
|||||||
|
|
||||||
## Communication
|
## 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 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.
|
* [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 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.
|
||||||
* [#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.
|
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|
||||||
|
@ -2,24 +2,9 @@
|
|||||||
|
|
||||||
## Minor Version Bumps
|
## 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:
|
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
|
||||||
|
|
||||||
```
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
### Update Static Libraries
|
### Update Static Libraries
|
||||||
|
|
||||||
@ -58,6 +43,27 @@ Submit a pull request to merge the `feature` branch into the `develop` branch in
|
|||||||
|
|
||||||
## All Releases
|
## 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
|
### Verify CI Build Status
|
||||||
|
|
||||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||||
|
@ -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.
|
Begin by installing all system packages required by NetBox and its dependencies.
|
||||||
|
|
||||||
!!! note
|
!!! 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
|
### Ubuntu
|
||||||
|
|
||||||
```no-highlight
|
```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
|
### CentOS
|
||||||
|
@ -140,7 +140,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
|
|||||||
|
|
||||||
## Troubleshooting LDAP
|
## 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`:
|
||||||
|
|
||||||
|
@ -11,6 +11,10 @@ The following sections detail how to set up a new instance of NetBox:
|
|||||||
5. [HTTP server](5-http-server.md)
|
5. [HTTP server](5-http-server.md)
|
||||||
6. [LDAP authentication](6-ldap.md) (optional)
|
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
|
## Requirements
|
||||||
|
|
||||||
| Dependency | Minimum Version |
|
| Dependency | Minimum Version |
|
||||||
|
@ -1,5 +1,65 @@
|
|||||||
# NetBox v2.10
|
# 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)
|
## v2.10.3 (2021-01-05)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
@ -40,14 +40,16 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
|
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
|
||||||
|
|
||||||
|
|
||||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
connected_endpoint = NestedInterfaceSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
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):
|
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||||
|
@ -690,6 +690,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||||
|
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||||
|
|
||||||
# InfiniBand
|
# InfiniBand
|
||||||
@ -806,6 +807,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||||
|
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
|
||||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -879,6 +881,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
TYPE_8P6C = '8p6c'
|
TYPE_8P6C = '8p6c'
|
||||||
TYPE_8P4C = '8p4c'
|
TYPE_8P4C = '8p4c'
|
||||||
TYPE_8P2C = '8p2c'
|
TYPE_8P2C = '8p2c'
|
||||||
|
TYPE_GG45 = 'gg45'
|
||||||
|
TYPE_TERA4P = 'tera-4p'
|
||||||
|
TYPE_TERA2P = 'tera-2p'
|
||||||
|
TYPE_TERA1P = 'tera-1p'
|
||||||
TYPE_110_PUNCH = '110-punch'
|
TYPE_110_PUNCH = '110-punch'
|
||||||
TYPE_BNC = 'bnc'
|
TYPE_BNC = 'bnc'
|
||||||
TYPE_MRJ21 = 'mrj21'
|
TYPE_MRJ21 = 'mrj21'
|
||||||
@ -904,6 +910,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_8P6C, '8P6C'),
|
(TYPE_8P6C, '8P6C'),
|
||||||
(TYPE_8P4C, '8P4C'),
|
(TYPE_8P4C, '8P4C'),
|
||||||
(TYPE_8P2C, '8P2C'),
|
(TYPE_8P2C, '8P2C'),
|
||||||
|
(TYPE_GG45, 'GG45'),
|
||||||
|
(TYPE_TERA4P, 'TERA 4P'),
|
||||||
|
(TYPE_TERA2P, 'TERA 2P'),
|
||||||
|
(TYPE_TERA1P, 'TERA 1P'),
|
||||||
(TYPE_110_PUNCH, '110 Punch'),
|
(TYPE_110_PUNCH, '110 Punch'),
|
||||||
(TYPE_BNC, 'BNC'),
|
(TYPE_BNC, 'BNC'),
|
||||||
(TYPE_MRJ21, 'MRJ21'),
|
(TYPE_MRJ21, 'MRJ21'),
|
||||||
@ -942,6 +952,8 @@ class CableTypeChoices(ChoiceSet):
|
|||||||
TYPE_CAT6 = 'cat6'
|
TYPE_CAT6 = 'cat6'
|
||||||
TYPE_CAT6A = 'cat6a'
|
TYPE_CAT6A = 'cat6a'
|
||||||
TYPE_CAT7 = 'cat7'
|
TYPE_CAT7 = 'cat7'
|
||||||
|
TYPE_CAT7A = 'cat7a'
|
||||||
|
TYPE_CAT8 = 'cat8'
|
||||||
TYPE_DAC_ACTIVE = 'dac-active'
|
TYPE_DAC_ACTIVE = 'dac-active'
|
||||||
TYPE_DAC_PASSIVE = 'dac-passive'
|
TYPE_DAC_PASSIVE = 'dac-passive'
|
||||||
TYPE_MRJ21_TRUNK = 'mrj21-trunk'
|
TYPE_MRJ21_TRUNK = 'mrj21-trunk'
|
||||||
@ -966,6 +978,8 @@ class CableTypeChoices(ChoiceSet):
|
|||||||
(TYPE_CAT6, 'CAT6'),
|
(TYPE_CAT6, 'CAT6'),
|
||||||
(TYPE_CAT6A, 'CAT6a'),
|
(TYPE_CAT6A, 'CAT6a'),
|
||||||
(TYPE_CAT7, 'CAT7'),
|
(TYPE_CAT7, 'CAT7'),
|
||||||
|
(TYPE_CAT7A, 'CAT7a'),
|
||||||
|
(TYPE_CAT8, 'CAT8'),
|
||||||
(TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
|
(TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
|
||||||
(TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
|
(TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
|
||||||
(TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'),
|
(TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'),
|
||||||
|
@ -264,7 +264,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
|
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1016,6 +1016,16 @@ class VirtualChassisFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='master__site__region',
|
field_name='master__site__region',
|
||||||
@ -1055,7 +1065,7 @@ class VirtualChassisFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'domain']
|
fields = ['id', 'domain', 'name']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -1142,7 +1152,7 @@ class ConnectionFilterSet:
|
|||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(device_id__in=value)
|
return queryset.filter(**{f'{name}__in': value})
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||||
|
@ -2352,6 +2352,11 @@ class ConsolePortCSVForm(CSVModelForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Port type'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@ -2425,6 +2430,11 @@ class ConsoleServerPortCSVForm(CSVModelForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=ConsolePortTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Port type'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
@ -2510,6 +2520,11 @@ class PowerPortCSVForm(CSVModelForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=PowerPortTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Port type'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@ -2630,6 +2645,11 @@ class PowerOutletCSVForm(CSVModelForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=PowerOutletTypeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='Outlet type'
|
||||||
|
)
|
||||||
power_port = CSVModelChoiceField(
|
power_port = CSVModelChoiceField(
|
||||||
queryset=PowerPort.objects.all(),
|
queryset=PowerPort.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -2687,6 +2707,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
mgmt_only = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
mac_address = forms.CharField(
|
mac_address = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='MAC address'
|
label='MAC address'
|
||||||
|
@ -363,3 +363,9 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label
|
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."
|
||||||
|
)
|
||||||
|
@ -198,7 +198,7 @@ class PathEndpoint(models.Model):
|
|||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
@ -234,7 +234,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
|||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks', 'custom_links')
|
||||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
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
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
@ -379,7 +379,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks', 'custom_links')
|
||||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
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)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
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
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks', 'custom_links')
|
||||||
class FrontPort(CableTermination, ComponentModel):
|
class FrontPort(CableTermination, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
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):
|
class RearPort(CableTermination, ComponentModel):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
@ -740,7 +740,7 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks', 'custom_links')
|
||||||
class DeviceBay(ComponentModel):
|
class DeviceBay(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
@ -800,7 +800,7 @@ class DeviceBay(ComponentModel):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||||
class InventoryItem(MPTTModel, ComponentModel):
|
class InventoryItem(MPTTModel, ComponentModel):
|
||||||
"""
|
"""
|
||||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||||
|
@ -299,6 +299,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
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
|
# Validate outer dimensions and unit
|
||||||
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_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")
|
raise ValidationError("Must specify a unit when setting an outer width/depth")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
|
||||||
@ -127,10 +128,18 @@ class DeviceTable(BaseTable):
|
|||||||
verbose_name='Type',
|
verbose_name='Type',
|
||||||
text=lambda record: record.device_type.display_name
|
text=lambda record: record.device_type.display_name
|
||||||
)
|
)
|
||||||
primary_ip = tables.Column(
|
if settings.PREFER_IPV4:
|
||||||
linkify=True,
|
primary_ip = tables.Column(
|
||||||
verbose_name='IP Address'
|
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(
|
primary_ip4 = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='IPv4 Address'
|
verbose_name='IPv4 Address'
|
||||||
@ -406,6 +415,7 @@ class BaseInterfaceTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||||
|
mgmt_only = BooleanColumn()
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='dcim:interface_list'
|
url_name='dcim:interface_list'
|
||||||
)
|
)
|
||||||
|
@ -95,6 +95,11 @@ CONSOLEPORT_BUTTONS = """
|
|||||||
{% if record.cable %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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-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>
|
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||||
|
@ -740,7 +740,10 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(
|
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 = (
|
device_bay_templates = (
|
||||||
|
@ -2399,9 +2399,9 @@ class VirtualChassisTestCase(TestCase):
|
|||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
virtual_chassis = (
|
virtual_chassis = (
|
||||||
VirtualChassis(master=devices[0], domain='Domain 1'),
|
VirtualChassis(name='VC 1', master=devices[0], domain='Domain 1'),
|
||||||
VirtualChassis(master=devices[2], domain='Domain 2'),
|
VirtualChassis(name='VC 2', master=devices[2], domain='Domain 2'),
|
||||||
VirtualChassis(master=devices[4], domain='Domain 3'),
|
VirtualChassis(name='VC 3', master=devices[4], domain='Domain 3'),
|
||||||
)
|
)
|
||||||
VirtualChassis.objects.bulk_create(virtual_chassis)
|
VirtualChassis.objects.bulk_create(virtual_chassis)
|
||||||
|
|
||||||
@ -2417,6 +2417,17 @@ class VirtualChassisTestCase(TestCase):
|
|||||||
params = {'domain': ['Domain 1', 'Domain 2']}
|
params = {'domain': ['Domain 1', 'Domain 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
@ -396,6 +396,7 @@ manufacturer: Generic
|
|||||||
model: TEST-1000
|
model: TEST-1000
|
||||||
slug: test-1000
|
slug: test-1000
|
||||||
u_height: 2
|
u_height: 2
|
||||||
|
subdevice_role: parent
|
||||||
comments: test comment
|
comments: test comment
|
||||||
console-ports:
|
console-ports:
|
||||||
- name: Console Port 1
|
- name: Console Port 1
|
||||||
@ -831,8 +832,8 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetypes = (
|
devicetypes = (
|
||||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
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'),
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
|
||||||
)
|
)
|
||||||
DeviceType.objects.bulk_create(devicetypes)
|
DeviceType.objects.bulk_create(devicetypes)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from cacheops import invalidate_model
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ class Command(BaseCommand):
|
|||||||
app_label, model_name = name.split('.')
|
app_label, model_name = name.split('.')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise CommandError(
|
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:
|
try:
|
||||||
app_config = apps.get_app_config(app_label)
|
app_config = apps.get_app_config(app_label)
|
||||||
@ -36,13 +37,13 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
model = app_config.get_model(model_name)
|
model = app_config.get_model(model_name)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
|
raise CommandError(f"Unknown model: {app_label}.{model_name}")
|
||||||
fields = [
|
fields = [
|
||||||
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
||||||
]
|
]
|
||||||
if not fields:
|
if not fields:
|
||||||
raise CommandError(
|
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(
|
models.append(
|
||||||
(model, fields)
|
(model, fields)
|
||||||
@ -67,7 +68,7 @@ class Command(BaseCommand):
|
|||||||
models = self._get_models(args)
|
models = self._get_models(args)
|
||||||
|
|
||||||
if options['verbosity']:
|
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 model, fields in models:
|
||||||
for field in fields:
|
for field in fields:
|
||||||
@ -78,7 +79,7 @@ class Command(BaseCommand):
|
|||||||
# Print the model and field name
|
# Print the model and field name
|
||||||
if options['verbosity']:
|
if options['verbosity']:
|
||||||
self.stdout.write(
|
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 ''
|
ending='\n' if options['verbosity'] >= 2 else ''
|
||||||
)
|
)
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
@ -89,23 +90,26 @@ class Command(BaseCommand):
|
|||||||
naturalized_value = naturalize(value, max_length=field.max_length)
|
naturalized_value = naturalize(value, max_length=field.max_length)
|
||||||
|
|
||||||
if options['verbosity'] >= 2:
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
|
self.stdout.write(f" {value} -> {naturalized_value}", ending='')
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
|
|
||||||
# Update each unique field value in bulk
|
# Update each unique field value in bulk
|
||||||
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
|
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
|
||||||
|
|
||||||
if options['verbosity'] >= 2:
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(" ({})".format(changed))
|
self.stdout.write(f" ({changed})")
|
||||||
count += changed
|
count += changed
|
||||||
|
|
||||||
# Print the total count of alterations for the field
|
# Print the total count of alterations for the field
|
||||||
if options['verbosity'] >= 2:
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
|
self.stdout.write(self.style.SUCCESS(
|
||||||
count, model._meta.verbose_name_plural, queryset.count()
|
f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
|
||||||
)))
|
))
|
||||||
elif options['verbosity']:
|
elif options['verbosity']:
|
||||||
self.stdout.write(self.style.SUCCESS(str(count)))
|
self.stdout.write(self.style.SUCCESS(str(count)))
|
||||||
|
|
||||||
|
# Invalidate cached queries
|
||||||
|
invalidate_model(model)
|
||||||
|
|
||||||
if options['verbosity']:
|
if options['verbosity']:
|
||||||
self.stdout.write(self.style.SUCCESS("Done."))
|
self.stdout.write(self.style.SUCCESS("Done."))
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
||||||
import random
|
import secrets
|
||||||
|
|
||||||
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
||||||
secure_random = random.SystemRandom()
|
print(''.join(secrets.choice(charset) for _ in range(50)))
|
||||||
print(''.join(secure_random.sample(charset, 50)))
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from django.conf import settings
|
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.shortcuts import get_object_or_404
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
@ -162,7 +164,12 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
# Create the new Prefix(es)
|
# Create the new Prefix(es)
|
||||||
if serializer.is_valid():
|
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.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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='get', responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||||
@swagger_auto_schema(method='post', responses={201: 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())
|
@action(detail=True, url_path='available-ips', methods=['get', 'post'], queryset=IPAddress.objects.all())
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||||
def available_ips(self, request, pk=None):
|
def available_ips(self, request, pk=None):
|
||||||
@ -225,7 +232,12 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
# Create the new IP address(es)
|
# Create the new IP address(es)
|
||||||
if serializer.is_valid():
|
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.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -192,7 +192,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
|||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
lookup_expr='family'
|
lookup_expr='family'
|
||||||
)
|
)
|
||||||
prefix = django_filters.CharFilter(
|
prefix = MultiValueCharFilter(
|
||||||
method='filter_prefix',
|
method='filter_prefix',
|
||||||
label='Prefix',
|
label='Prefix',
|
||||||
)
|
)
|
||||||
@ -304,13 +304,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def filter_prefix(self, queryset, name, value):
|
def filter_prefix(self, queryset, name, value):
|
||||||
if not value.strip():
|
query_values = []
|
||||||
return queryset
|
for v in value:
|
||||||
try:
|
try:
|
||||||
query = str(netaddr.IPNetwork(value).cidr)
|
query_values.append(netaddr.IPNetwork(v))
|
||||||
return queryset.filter(prefix=query)
|
except (AddrFormatError, ValueError):
|
||||||
except (AddrFormatError, ValueError):
|
pass
|
||||||
return queryset.none()
|
return queryset.filter(prefix__in=query_values)
|
||||||
|
|
||||||
def search_within(self, queryset, name, value):
|
def search_within(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
@ -734,13 +734,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Enforce unique IP space (if applicable)
|
# Enforce unique IP space (if applicable)
|
||||||
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
|
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
|
||||||
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
|
||||||
) or (
|
|
||||||
self.vrf and self.vrf.enforce_unique
|
|
||||||
)):
|
|
||||||
duplicate_ips = self.get_duplicates()
|
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({
|
raise ValidationError({
|
||||||
'address': "Duplicate IP address found in {}: {}".format(
|
'address': "Duplicate IP address found in {}: {}".format(
|
||||||
"VRF {}".format(self.vrf) if self.vrf else "global table",
|
"VRF {}".format(self.vrf) if self.vrf else "global table",
|
||||||
|
@ -37,7 +37,7 @@ IPADDRESS_LINK = """
|
|||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||||
{% elif perms.ipam.add_ipaddress %}
|
{% 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 %}
|
{% else %}
|
||||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -50,8 +50,8 @@ IPADDRESS_ASSIGN_LINK = """
|
|||||||
VRF_LINK = """
|
VRF_LINK = """
|
||||||
{% if record.vrf %}
|
{% if record.vrf %}
|
||||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||||
{% elif prefix.vrf %}
|
{% elif object.vrf %}
|
||||||
{{ prefix.vrf }}
|
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
Global
|
Global
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -114,6 +114,8 @@ TENANT_LINK = """
|
|||||||
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
|
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
|
||||||
{% elif record.vrf.tenant %}
|
{% elif record.vrf.tenant %}
|
||||||
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
|
<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 %}
|
{% else %}
|
||||||
—
|
—
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -431,6 +433,9 @@ class InterfaceIPAddressTable(BaseTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=TENANT_LINK
|
template_code=TENANT_LINK
|
||||||
)
|
)
|
||||||
|
actions = ButtonsColumn(
|
||||||
|
model=IPAddress
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
|
@ -422,6 +422,11 @@ class PrefixTestCase(TestCase):
|
|||||||
params = {'family': '6'}
|
params = {'family': '6'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
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):
|
def test_is_pool(self):
|
||||||
params = {'is_pool': 'true'}
|
params = {'is_pool': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -259,6 +259,18 @@ class TestIPAddress(TestCase):
|
|||||||
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
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)
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
|
||||||
def test_duplicate_nonunique_role(self):
|
def test_duplicate_nonunique_role(self):
|
||||||
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
|
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
|
||||||
|
@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
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(
|
import_targets_table = tables.RouteTargetTable(
|
||||||
instance.import_targets.prefetch_related('tenant'),
|
instance.import_targets.prefetch_related('tenant'),
|
||||||
@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'prefix_count': prefix_count,
|
'prefix_count': prefix_count,
|
||||||
|
'ipaddress_count': ipaddress_count,
|
||||||
'import_targets_table': import_targets_table,
|
'import_targets_table': import_targets_table,
|
||||||
'export_targets_table': export_targets_table,
|
'export_targets_table': export_targets_table,
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.10.3'
|
VERSION = '2.10.6'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -398,6 +398,7 @@ if CACHING_REDIS_USING_SENTINEL:
|
|||||||
'locations': CACHING_REDIS_SENTINELS,
|
'locations': CACHING_REDIS_SENTINELS,
|
||||||
'service_name': CACHING_REDIS_SENTINEL_SERVICE,
|
'service_name': CACHING_REDIS_SENTINEL_SERVICE,
|
||||||
'db': CACHING_REDIS_DATABASE,
|
'db': CACHING_REDIS_DATABASE,
|
||||||
|
'password': CACHING_REDIS_PASSWORD,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
if CACHING_REDIS_SSL:
|
if CACHING_REDIS_SSL:
|
||||||
|
@ -792,7 +792,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
if form.cleaned_data[name]:
|
if form.cleaned_data[name]:
|
||||||
getattr(obj, name).set(form.cleaned_data[name])
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
# Normal fields
|
# 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])
|
setattr(obj, name, form.cleaned_data[name])
|
||||||
|
|
||||||
# Update custom fields
|
# Update custom fields
|
||||||
|
@ -177,6 +177,10 @@ nav ul.pagination {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 8px !important;
|
margin-bottom: 8px !important;
|
||||||
}
|
}
|
||||||
|
.pagination > li > a > .mdi::before {
|
||||||
|
top: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Devices */
|
/* Devices */
|
||||||
table.component-list td.subtable {
|
table.component-list td.subtable {
|
||||||
|
@ -69,7 +69,8 @@
|
|||||||
<h5>Total segments: {{ traced_path|length }}</h5>
|
<h5>Total segments: {{ traced_path|length }}</h5>
|
||||||
<h5>Total length:
|
<h5>Total length:
|
||||||
{% if total_length %}
|
{% if total_length %}
|
||||||
{{ total_length|floatformat:"-2" }} Meters
|
{{ total_length|floatformat:"-2" }} Meters /
|
||||||
|
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">N/A</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -204,7 +204,7 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Utilization</strong>
|
<strong>Power Utilization</strong>
|
||||||
@ -217,10 +217,10 @@
|
|||||||
<th>Available</th>
|
<th>Available</th>
|
||||||
<th>Utilization</th>
|
<th>Utilization</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for pp in power_ports %}
|
{% for powerport in object.powerports.all %}
|
||||||
{% with utilization=pp.get_power_draw powerfeed=pp.connected_endpoint %}
|
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoint %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pp }}</td>
|
<td>{{ powerport }}</td>
|
||||||
<td>{{ utilization.outlet_count }}</td>
|
<td>{{ utilization.outlet_count }}</td>
|
||||||
<td>{{ utilization.allocated }}VA</td>
|
<td>{{ utilization.allocated }}VA</td>
|
||||||
{% if powerfeed.available_power %}
|
{% if powerfeed.available_power %}
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
<li role="presentation" {% if active_tab == 'device' %} class="active"{% endif %}>
|
<li role="presentation" {% if active_tab == 'device' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device' pk=object.pk %}">Device</a>
|
<a href="{% url 'dcim:device' pk=object.pk %}">Device</a>
|
||||||
</li>
|
</li>
|
||||||
{% with interface_count=object.interfaces.count %}
|
{% with interface_count=object.vc_interfaces.count %}
|
||||||
{% if interface_count %}
|
{% if interface_count %}
|
||||||
<li role="presentation" {% if active_tab == 'interfaces' %} class="active"{% endif %}>
|
<li role="presentation" {% if active_tab == 'interfaces' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
|
<a href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load perms %}
|
{% load perms %}
|
||||||
|
{% load custom_links %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@ -30,6 +31,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}{{ object.device }} / {{ object }}{% endblock %}</h1>
|
<h1>{% block title %}{{ object.device }} / {{ object }}{% endblock %}</h1>
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% custom_links object %}
|
||||||
|
</div>
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
<a href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
<a href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
||||||
|
@ -9,8 +9,3 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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 %}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends 'dcim/device_component.html' %}
|
{% extends 'dcim/device_component.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -226,7 +227,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -127,22 +127,20 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% with rack=object.rack %}
|
<div class="row" style="margin-bottom: 20px">
|
||||||
<div class="row" style="margin-bottom: 20px">
|
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
<div class="rack_header">
|
||||||
<div class="rack_header">
|
<h4>Front</h4>
|
||||||
<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>
|
</div>
|
||||||
|
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
|
||||||
</div>
|
</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 %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% 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>
|
<strong>
|
||||||
<a href="{% url 'dcim:cable' pk=cable.pk %}">
|
<a href="{% url 'dcim:cable' pk=cable.pk %}">
|
||||||
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
||||||
|
@ -95,6 +95,12 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
|
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IP Addresses</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
@ -93,7 +94,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -24,7 +24,7 @@ class TenantGroupSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
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)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
ipaddress_count = serializers.IntegerField(read_only=True)
|
ipaddress_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -56,6 +56,7 @@ class TenantTest(APIViewTestCases.APIViewTestCase):
|
|||||||
model = Tenant
|
model = Tenant
|
||||||
brief_fields = ['id', 'name', 'slug', 'url']
|
brief_fields = ['id', 'name', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
|
'group': None,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ class ObjectTypeListFilter(admin.SimpleListFilter):
|
|||||||
parameter_name = 'object_type'
|
parameter_name = 'object_type'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
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')
|
content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
|
||||||
return [
|
return [
|
||||||
(ct.pk, ct) for ct in content_types
|
(ct.pk, ct) for ct in content_types
|
||||||
|
@ -28,29 +28,38 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
|||||||
serializer = super().get_request_serializer()
|
serializer = super().get_request_serializer()
|
||||||
|
|
||||||
if serializer is not None and self.method in self.implicit_body_methods:
|
if serializer is not None and self.method in self.implicit_body_methods:
|
||||||
properties = {}
|
writable_class = self.get_writable_class(serializer)
|
||||||
for child_name, child in serializer.fields.items():
|
if writable_class is not None:
|
||||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
if hasattr(serializer, 'child'):
|
||||||
properties[child_name] = None
|
child_serializer = self.get_writable_class(serializer.child)
|
||||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
serializer = writable_class(child=child_serializer)
|
||||||
properties[child_name] = None
|
else:
|
||||||
|
serializer = writable_class()
|
||||||
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()
|
|
||||||
|
|
||||||
return serializer
|
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):
|
class SerializedPKRelatedFieldInspector(FieldInspector):
|
||||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||||
|
@ -5,6 +5,7 @@ from io import StringIO
|
|||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
|
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
from django.db.models import Count
|
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
|
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.
|
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):
|
class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField):
|
||||||
|
@ -114,7 +114,10 @@ class ContentTypeSelect(StaticSelect2):
|
|||||||
class NumericArrayField(SimpleArrayField):
|
class NumericArrayField(SimpleArrayField):
|
||||||
|
|
||||||
def to_python(self, value):
|
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)
|
return super().to_python(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,6 +220,14 @@ def as_range(n):
|
|||||||
return 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
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -68,7 +68,6 @@ class NestedVaporVLANSerializer(WritableNestedSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
virtual_circuit = SerializedPKRelatedField(
|
virtual_circuit = SerializedPKRelatedField(
|
||||||
source='vlan_of',
|
|
||||||
queryset=VirtualCircuitVLAN.objects.all(),
|
queryset=VirtualCircuitVLAN.objects.all(),
|
||||||
serializer=NestedVirtualCircuitSerializer,
|
serializer=NestedVirtualCircuitSerializer,
|
||||||
pk_field='vlan',
|
pk_field='vlan',
|
||||||
|
@ -4,7 +4,6 @@ from rest_framework.decorators import action
|
|||||||
|
|
||||||
from dcim.api.views import PathEndpointMixin
|
from dcim.api.views import PathEndpointMixin
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from dcim.filters import InterfaceFilterSet
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from tenancy.models import Tenant as Customer
|
from tenancy.models import Tenant as Customer
|
||||||
from netbox.api.views import ModelViewSet
|
from netbox.api.views import ModelViewSet
|
||||||
@ -26,5 +25,5 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = InterfaceFilterSet
|
filterset_class = filters.InterfaceFilter
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
@ -127,21 +127,21 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
vc_context = django_filters.ModelMultipleChoiceFilter(
|
vc_context = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='tagged_vlans__vlan_of__virtual_circuit__context',
|
field_name='tagged_vlans__virtual_circuit__virtual_circuit__context',
|
||||||
queryset=VirtualCircuit.objects.all(),
|
queryset=VirtualCircuit.objects.all(),
|
||||||
to_field_name='context',
|
to_field_name='context',
|
||||||
label='Virtual Circuit (context)',
|
label='Virtual Circuit (context)',
|
||||||
)
|
)
|
||||||
|
|
||||||
vc_name = django_filters.ModelMultipleChoiceFilter(
|
vc_name = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='tagged_vlans__vlan_of__virtual_circuit__name',
|
field_name='tagged_vlans__virtual_circuit__virtual_circuit__name',
|
||||||
queryset=VirtualCircuit.objects.all(),
|
queryset=VirtualCircuit.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Virtual Circuit (name)',
|
label='Virtual Circuit (name)',
|
||||||
)
|
)
|
||||||
|
|
||||||
vc_id = django_filters.ModelMultipleChoiceFilter(
|
vc_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='tagged_vlans__vlan_of__virtual_circuit__vcid',
|
field_name='tagged_vlans__virtual_circuit__virtual_circuit__vcid',
|
||||||
queryset=VirtualCircuit.objects.all(),
|
queryset=VirtualCircuit.objects.all(),
|
||||||
to_field_name='vcid',
|
to_field_name='vcid',
|
||||||
label='Virtual Circuit (vcid)',
|
label='Virtual Circuit (vcid)',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.conf import settings
|
||||||
from dcim.tables.devices import BaseInterfaceTable
|
from dcim.tables.devices import BaseInterfaceTable
|
||||||
from tenancy.tables import COL_TENANT
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import (
|
from utilities.tables import (
|
||||||
@ -125,10 +125,18 @@ class VirtualMachineDetailTable(VirtualMachineTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='IPv6 Address'
|
verbose_name='IPv6 Address'
|
||||||
)
|
)
|
||||||
primary_ip = tables.Column(
|
if settings.PREFER_IPV4:
|
||||||
linkify=True,
|
primary_ip = tables.Column(
|
||||||
verbose_name='IP Address'
|
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(
|
tags = TagColumn(
|
||||||
url_name='virtualization:virtualmachine_list'
|
url_name='virtualization:virtualmachine_list'
|
||||||
)
|
)
|
||||||
|
@ -396,3 +396,6 @@ class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
|
|||||||
model_form = forms.VMInterfaceForm
|
model_form = forms.VMInterfaceForm
|
||||||
filterset = filters.VirtualMachineFilterSet
|
filterset = filters.VirtualMachineFilterSet
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
|
|
||||||
|
def get_required_permission(self):
|
||||||
|
return f'virtualization.add_vminterface'
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
Django==3.1.3
|
Django==3.1.7
|
||||||
django-cacheops==5.1
|
django-cacheops==5.1
|
||||||
django-cors-headers==3.5.0
|
django-cors-headers==3.7.0
|
||||||
django-debug-toolbar==3.1.1
|
django-debug-toolbar==3.2
|
||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
django-mptt==0.11.0
|
django-mptt==0.12.0
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.1.0
|
django-prometheus==2.1.0
|
||||||
django-rq==2.4.0
|
django-rq==2.4.0
|
||||||
django-tables2==2.3.3
|
django-tables2==2.3.4
|
||||||
django-taggit==1.3.0
|
django-taggit==1.3.0
|
||||||
django-timezone-field==4.0
|
django-timezone-field==4.1.1
|
||||||
djangorestframework==3.12.2
|
djangorestframework==3.12.2
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.20.0
|
||||||
gunicorn==20.0.4
|
gunicorn==20.0.4
|
||||||
Jinja2==2.11.2
|
Jinja2==2.11.3
|
||||||
Markdown==3.3.3
|
Markdown==3.3.4
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.0.1
|
Pillow==8.1.2
|
||||||
psycopg2-binary==2.8.6
|
psycopg2-binary==2.8.6
|
||||||
pycryptodome==3.9.9
|
pycryptodome==3.10.1
|
||||||
PyYAML==5.3.1
|
PyYAML==5.4.1
|
||||||
svgwrite==1.4
|
svgwrite==1.4.1
|
||||||
|
12
upgrade.sh
12
upgrade.sh
@ -29,19 +29,25 @@ eval $COMMAND || {
|
|||||||
# Activate the virtual environment
|
# Activate the virtual environment
|
||||||
source "${VIRTUALENV}/bin/activate"
|
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
|
# Install necessary system packages
|
||||||
COMMAND="pip3 install wheel"
|
COMMAND="pip install wheel"
|
||||||
echo "Installing Python system packages ($COMMAND)..."
|
echo "Installing Python system packages ($COMMAND)..."
|
||||||
eval $COMMAND || exit 1
|
eval $COMMAND || exit 1
|
||||||
|
|
||||||
# Install required Python packages
|
# Install required Python packages
|
||||||
COMMAND="pip3 install -r requirements.txt"
|
COMMAND="pip install -r requirements.txt"
|
||||||
echo "Installing core dependencies ($COMMAND)..."
|
echo "Installing core dependencies ($COMMAND)..."
|
||||||
eval $COMMAND || exit 1
|
eval $COMMAND || exit 1
|
||||||
|
|
||||||
# Install optional packages (if any)
|
# Install optional packages (if any)
|
||||||
if [ -s "local_requirements.txt" ]; then
|
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)..."
|
echo "Installing local dependencies ($COMMAND)..."
|
||||||
eval $COMMAND || exit 1
|
eval $COMMAND || exit 1
|
||||||
elif [ -f "local_requirements.txt" ]; then
|
elif [ -f "local_requirements.txt" ]; then
|
||||||
|
Loading…
Reference in New Issue
Block a user