mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-31 06:08:19 +01:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d286fbdc3 | ||
|
|
afc58e6bff | ||
|
|
f59b5119e5 | ||
|
|
8eca7377a5 | ||
|
|
e639de9861 | ||
|
|
9a4d885a9c | ||
|
|
d28cc4fdb5 | ||
|
|
54a979c936 | ||
|
|
d18652f726 | ||
|
|
ac1083f65d | ||
|
|
bcfc2e5f74 | ||
|
|
a135396d7b | ||
|
|
ef987bd008 | ||
|
|
a2fef1df83 | ||
|
|
016c7d4e6f | ||
|
|
0a60bcdb8d | ||
|
|
5374e6c881 | ||
|
|
ab65ab860f | ||
|
|
ad2742dbb2 | ||
|
|
997077fc5d | ||
|
|
c8042ad511 | ||
|
|
b51d66ea72 | ||
|
|
0321ae0a7f | ||
|
|
e4ecd2dae9 | ||
|
|
335cfcde57 | ||
|
|
91fe80f73c | ||
|
|
7e437455c1 | ||
|
|
f9f1a4439f | ||
|
|
68082a88a7 | ||
|
|
a94e5c7403 | ||
|
|
c061f7021e | ||
|
|
ad43373f5b | ||
|
|
720b88048d | ||
|
|
d58291d119 | ||
|
|
132b1ff479 | ||
|
|
cb9478e0ea | ||
|
|
04964cc52b | ||
|
|
9990fd25d4 | ||
|
|
e9930854c4 | ||
|
|
11ac4cd2c2 | ||
|
|
5b07c77708 | ||
|
|
c3ad2d0a80 | ||
|
|
1cc82f2ab7 | ||
|
|
358d7ac562 | ||
|
|
cae784ff52 | ||
|
|
835e11d10f | ||
|
|
e7157973e7 | ||
|
|
5b5110acbf | ||
|
|
61e5eff666 | ||
|
|
16a3d1339a | ||
|
|
759190d8ba | ||
|
|
f8e2cb06d1 | ||
|
|
37aa10c5a5 | ||
|
|
ad9ed13b9a | ||
|
|
d9f8d809c8 | ||
|
|
03054b88aa | ||
|
|
a72911b527 | ||
|
|
8e4da3faf4 | ||
|
|
46945aa98d | ||
|
|
939224d0af | ||
|
|
8d15f79f1e | ||
|
|
b079cc12f4 | ||
|
|
ad1da22257 | ||
|
|
fa8e70fe26 | ||
|
|
a87f34ebb3 | ||
|
|
75d875615b | ||
|
|
f63cf6a7a4 | ||
|
|
18ab1144bb | ||
|
|
a9b216d212 | ||
|
|
6b19a1ece9 | ||
|
|
0dd6d552a8 | ||
|
|
cfddf570b9 | ||
|
|
d2c8aae59c | ||
|
|
b1cd634ab4 | ||
|
|
02e8979178 |
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
|
||||
40
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: 📖 Documentation Change
|
||||
about: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Change Type
|
||||
description: What type of change are you proposing?
|
||||
options:
|
||||
- Addition
|
||||
- Correction
|
||||
- Removal
|
||||
- Cleanup (formatting, typos, etc.)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Area
|
||||
description: To what section of the documentation does this change primarily pertain?
|
||||
options:
|
||||
- Installation instructions
|
||||
- Configuration parameters
|
||||
- Functionality/features
|
||||
- REST API
|
||||
- Administration/development
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Changes
|
||||
description: "Describe the proposed changes and why they are necessary"
|
||||
validations:
|
||||
required: true
|
||||
- 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.
|
||||
@@ -25,7 +25,7 @@ discussions.
|
||||
|
||||
### Slack
|
||||
|
||||
For real-time chat, you can join the **#netbox** Slack channel on [NetworkToCode](https://slack.networktocode.com/).
|
||||
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://slack.netbox.dev/).
|
||||
Unfortunately, the Slack channel does not provide long-term retention of chat
|
||||
history, so try to avoid it for any discussions would benefit from being
|
||||
preserved for future reference.
|
||||
@@ -185,11 +185,5 @@ overlooked.
|
||||
sync to review agenda items. This meeting provides opportunity to present and
|
||||
discuss pressing topics. Meetings are held as virtual audio/video conferences.
|
||||
|
||||
* Official channels for communication include:
|
||||
|
||||
* GitHub issues, pull requests, and discussions
|
||||
* The [netbox-discuss](https://groups.google.com/g/netbox-discuss) mailing list
|
||||
* The **#netbox** channel on [NetworkToCode Slack](https://networktocode.slack.com/)
|
||||
|
||||
* Maintainers with no substantial recorded activity in a 60-day period will be
|
||||
removed from the project.
|
||||
|
||||
@@ -12,7 +12,11 @@ complete list of requirements, see `requirements.txt`. The code is available [on
|
||||
|
||||
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/).
|
||||
|
||||
Questions? Comments? Start by perusing our [GitHub discussions](https://github.com/netbox-community/netbox/discussions) for the topic you have in mind.
|
||||
### Discussion
|
||||
|
||||
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
|
||||
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
|
||||
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
|
||||
|
||||
### Build Status
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Caching
|
||||
|
||||
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../../configuration/optional-settings/#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
|
||||
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../configuration/optional-settings.md#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
|
||||
|
||||
If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
By default, the [`NAPALM_USERNAME`](../configuration/optional-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/optional-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
|
||||
@@ -12,7 +12,7 @@ A NetBox report is a mechanism for validating the integrity of data within NetBo
|
||||
|
||||
## Writing Reports
|
||||
|
||||
Reports must be saved as files in the [`REPORTS_ROOT`](../../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
|
||||
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
|
||||
|
||||
!!! warning
|
||||
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
|
||||
|
||||
@@ -68,7 +68,7 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues.
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
|
||||
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
|
||||
|
||||
|
||||
@@ -12,13 +12,16 @@ NetBox employs a [PostgreSQL](https://www.postgresql.org/) database, so general
|
||||
Use the `pg_dump` utility to export the entire database to a file:
|
||||
|
||||
```no-highlight
|
||||
pg_dump netbox > netbox.sql
|
||||
pg_dump --username netbox --password --host localhost netbox > netbox.sql
|
||||
```
|
||||
|
||||
!!! note
|
||||
You may need to change the username, host, and/or database in the command above to match your installation.
|
||||
|
||||
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||
|
||||
```no-highlight
|
||||
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||
pg_dump ... --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||
```
|
||||
|
||||
### Load an Exported Database
|
||||
@@ -41,7 +44,7 @@ Keep in mind that PostgreSQL user accounts and permissions are not included with
|
||||
If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following:
|
||||
|
||||
```no-highlight
|
||||
pg_dump -s netbox > netbox_schema.sql
|
||||
pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -281,6 +281,14 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
|
||||
|
||||
---
|
||||
|
||||
## MAPS_URL
|
||||
|
||||
Default: `https://maps.google.com/?q=` (Google Maps)
|
||||
|
||||
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
|
||||
|
||||
---
|
||||
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
Default: 1000
|
||||
@@ -301,7 +309,7 @@ The file path to the location where media files (such as image attachments) are
|
||||
|
||||
Default: False
|
||||
|
||||
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../../additional-features/prometheus-metrics/) documentation for more details.
|
||||
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
|
||||
|
||||
* A Linux system or environment
|
||||
* A PostgreSQL server, which can be installed locally [per the documentation](/installation/1-postgresql/)
|
||||
* A Redis server, which can also be [installed locally](/installation/2-redis/)
|
||||
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
|
||||
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
|
||||
* A supported version of Python
|
||||
|
||||
### Fork the Repo
|
||||
|
||||
@@ -4,12 +4,12 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
|
||||
|
||||
## Communication
|
||||
|
||||
Communication among developers should always occur via public channels:
|
||||
There are several official forums for communication among the developers and community members:
|
||||
|
||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
||||
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [The mailing list](https://groups.google.com/g/netbox-discuss) - An alternative forum for general discussion (GitHub is preferred).
|
||||
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://slack.netbox.dev/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
|
||||
|
||||
## Governance
|
||||
|
||||
|
||||
@@ -2,24 +2,9 @@
|
||||
|
||||
## Minor Version Bumps
|
||||
|
||||
### Update Requirements
|
||||
### Address Pinned Dependencies
|
||||
|
||||
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
|
||||
|
||||
```
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
|
||||
|
||||
Every minor version release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
|
||||
|
||||
1. Create a new virtual environment.
|
||||
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
|
||||
3. Run all tests and check that the UI and API function as expected.
|
||||
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
5. Update the package versions in `requirements.txt` as appropriate.
|
||||
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
|
||||
|
||||
### Update Static Libraries
|
||||
|
||||
@@ -58,6 +43,27 @@ Submit a pull request to merge the `feature` branch into the `develop` branch in
|
||||
|
||||
## All Releases
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
|
||||
|
||||
```
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
|
||||
|
||||
Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
|
||||
|
||||
1. Create a new virtual environment.
|
||||
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
|
||||
3. Run all tests and check that the UI and API function as expected.
|
||||
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
5. Update the package versions in `requirements.txt` as appropriate.
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be pinned to its current minor version in `base_requirements.txt` (with an explanatory comment) and revisited for the next major NetBox release.
|
||||
|
||||
### Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||
|
||||
@@ -51,7 +51,7 @@ $ sudo -u postgres psql
|
||||
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
||||
Type "help" for help.
|
||||
|
||||
postgres=# CREATE DATABASE netbox ENCODING 'UTF8' LC_COLLATE='C.UTF-8' LC_CTYPE='C.UTF-8';
|
||||
postgres=# CREATE DATABASE netbox;
|
||||
CREATE DATABASE
|
||||
postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
|
||||
CREATE ROLE
|
||||
|
||||
@@ -113,7 +113,7 @@ cd /opt/netbox/netbox/netbox/
|
||||
sudo cp configuration.example.py configuration.py
|
||||
```
|
||||
|
||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](/configuration/), but only the following four are required for new installations:
|
||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
||||
|
||||
* `ALLOWED_HOSTS`
|
||||
* `DATABASE`
|
||||
@@ -136,7 +136,7 @@ ALLOWED_HOSTS = ['*']
|
||||
|
||||
### DATABASE
|
||||
|
||||
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](/configuration/required-settings/#database) for more detail on individual parameters.
|
||||
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-settings.md#database) for more detail on individual parameters.
|
||||
|
||||
```python
|
||||
DATABASE = {
|
||||
@@ -151,7 +151,7 @@ DATABASE = {
|
||||
|
||||
### REDIS
|
||||
|
||||
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](/configuration/required-settings/#redis) for more detail on individual parameters.
|
||||
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-settings.md#redis) for more detail on individual parameters.
|
||||
|
||||
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
||||
|
||||
@@ -203,7 +203,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt
|
||||
|
||||
### Remote File Storage
|
||||
|
||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](/configuration/optional-settings/#storage_backend) in `configuration.py`.
|
||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
|
||||
|
||||
```no-highlight
|
||||
sudo echo django-storages >> /opt/netbox/local_requirements.txt
|
||||
|
||||
@@ -142,7 +142,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
|
||||
|
||||
`systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
|
||||
|
||||
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`:
|
||||
For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/optional-settings.md#logging) configuration to `configuration.py`:
|
||||
|
||||
```python
|
||||
LOGGING = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Review the Release Notes
|
||||
|
||||
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
|
||||
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
|
||||
|
||||
## Update Dependencies to Required Versions
|
||||
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
# NetBox v2.10
|
||||
|
||||
## v2.10.8 (2021-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6060](https://github.com/netbox-community/netbox/issues/6060) - Fix exception on cable trace in UI (regression from #5650)
|
||||
|
||||
---
|
||||
|
||||
## v2.10.7 (2021-03-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5641](https://github.com/netbox-community/netbox/issues/5641) - Allow filtering device components by label
|
||||
* [#5723](https://github.com/netbox-community/netbox/issues/5723) - Allow customization of the geographic mapping service via `MAPS_URL` config parameter
|
||||
* [#5736](https://github.com/netbox-community/netbox/issues/5736) - Allow changing site assignment when bulk editing devices
|
||||
* [#5953](https://github.com/netbox-community/netbox/issues/5953) - Support Markdown rendering for custom script descriptions
|
||||
* [#6040](https://github.com/netbox-community/netbox/issues/6040) - Add UI search fields for asset tag for devices and racks
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image
|
||||
* [#5650](https://github.com/netbox-community/netbox/issues/5650) - Denote when the total length of a cable trace may exceed the indicated value
|
||||
* [#5962](https://github.com/netbox-community/netbox/issues/5962) - Ensure consistent display of change log action labels
|
||||
* [#5966](https://github.com/netbox-community/netbox/issues/5966) - Skip Markdown reference link when tabbing through form fields
|
||||
* [#5977](https://github.com/netbox-community/netbox/issues/5977) - Correct validation of `RELEASE_CHECK_URL` config parameter
|
||||
* [#6006](https://github.com/netbox-community/netbox/issues/6006) - Fix VLAN group/site association for bulk prefix import
|
||||
* [#6010](https://github.com/netbox-community/netbox/issues/6010) - Eliminate duplicate virtual chassis search results
|
||||
* [#6012](https://github.com/netbox-community/netbox/issues/6012) - Pre-populate attributes when creating an available child prefix via the UI
|
||||
* [#6023](https://github.com/netbox-community/netbox/issues/6023) - Fix display of bottom banner with uBlock Origin enabled
|
||||
|
||||
---
|
||||
|
||||
## v2.10.6 (2021-03-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5592](https://github.com/netbox-community/netbox/issues/5592) - Add IP addresses count to VRF view
|
||||
* [#5630](https://github.com/netbox-community/netbox/issues/5630) - Add QSFP+ (64GFC) FibreChannel Interface option
|
||||
* [#5884](https://github.com/netbox-community/netbox/issues/5884) - Enable custom links for device components
|
||||
* [#5914](https://github.com/netbox-community/netbox/issues/5914) - Add edit/delete buttons for IP addresses on interface view
|
||||
* [#5942](https://github.com/netbox-community/netbox/issues/5942) - Add button to add a new IP address on interface view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5703](https://github.com/netbox-community/netbox/issues/5703) - Fix VRF and Tenant field population when adding IP addresses from prefix
|
||||
* [#5819](https://github.com/netbox-community/netbox/issues/5819) - Enable ordering of virtual machines by primary IP address
|
||||
* [#5872](https://github.com/netbox-community/netbox/issues/5872) - Ordering of devices by primary IP should respect `PREFER_IPV4` configuration parameter
|
||||
* [#5922](https://github.com/netbox-community/netbox/issues/5922) - Fix options for filtering object permissions in admin UI
|
||||
* [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values
|
||||
* [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize`
|
||||
|
||||
---
|
||||
|
||||
## v2.10.5 (2021-02-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -20,7 +20,7 @@ http://netbox/api/dcim/sites/
|
||||
}
|
||||
```
|
||||
|
||||
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../../configuration/optional-settings/#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
|
||||
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
|
||||
|
||||
```
|
||||
$ curl http://netbox/api/dcim/sites/
|
||||
|
||||
@@ -269,7 +269,7 @@ The brief format is supported for both lists and individual objects.
|
||||
|
||||
### Excluding Config Contexts
|
||||
|
||||
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
|
||||
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
|
||||
|
||||
## Pagination
|
||||
|
||||
@@ -308,7 +308,7 @@ Vary: Accept
|
||||
}
|
||||
```
|
||||
|
||||
The default page is determined by the [`PAGINATE_COUNT`](../../configuration/optional-settings/#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
The default page is determined by the [`PAGINATE_COUNT`](../configuration/optional-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
|
||||
```
|
||||
http://netbox/api/dcim/devices/?limit=100
|
||||
@@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../../configuration/optional-settings/#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
|
||||
!!! warning
|
||||
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
||||
@@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.'
|
||||
|
||||
### Creating a New Object
|
||||
|
||||
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
|
||||
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/index.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
|
||||
|
||||
```no-highlight
|
||||
curl -s -X POST \
|
||||
|
||||
@@ -4,7 +4,7 @@ As with most other objects, the REST API can be used to view, create, modify, an
|
||||
|
||||
## Generating a Session Key
|
||||
|
||||
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`.
|
||||
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../core-functionality/secrets.md#user-keys). The private key must be POSTed with the name `private_key`.
|
||||
|
||||
```no-highlight
|
||||
$ curl -X POST http://netbox/api/secrets/get-session-key/ \
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@@ -13,13 +14,12 @@ from dcim.models import (
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.utils import decompile_path_node
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api import (
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@@ -98,7 +98,7 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
time_zone = TimeZoneSerializerField(required=False)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -686,6 +686,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
|
||||
# InfiniBand
|
||||
@@ -801,6 +802,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
|
||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -783,7 +783,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(
|
||||
@@ -799,7 +799,7 @@ class ConsoleServerPortFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
@@ -810,7 +810,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description']
|
||||
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
@@ -821,7 +821,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'name', 'feed_leg', 'description']
|
||||
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
@@ -867,7 +867,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
|
||||
fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
@@ -921,21 +921,21 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
fields = ['id', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'name', 'type', 'positions', 'description']
|
||||
fields = ['id', 'name', 'label', 'type', 'positions', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
@@ -996,7 +996,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ['id', 'name', 'part_id', 'asset_tag', 'discovered']
|
||||
fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1075,7 +1075,7 @@ class VirtualChassisFilterSet(BaseFilterSet):
|
||||
Q(members__name__icontains=value) |
|
||||
Q(domain__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
class CableFilterSet(BaseFilterSet):
|
||||
|
||||
@@ -60,12 +60,18 @@ def get_device_by_name_or_pk(name):
|
||||
|
||||
class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
|
||||
field_order = [
|
||||
'q', 'region', 'site'
|
||||
'q', 'name', 'label', 'region', 'site'
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
label = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
@@ -659,7 +665,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Rack
|
||||
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
|
||||
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant', 'asset_tag']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -707,6 +713,9 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
required=False,
|
||||
null_option='None'
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -944,10 +953,10 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
widgets = {
|
||||
'subdevice_role': StaticSelect2(),
|
||||
# Exclude SVG images (unsupported by PIL)
|
||||
'front_image': forms.FileInput(attrs={
|
||||
'front_image': forms.ClearableFileInput(attrs={
|
||||
'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff'
|
||||
}),
|
||||
'rear_image': forms.FileInput(attrs={
|
||||
'rear_image': forms.ClearableFileInput(attrs={
|
||||
'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff'
|
||||
})
|
||||
}
|
||||
@@ -2088,6 +2097,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
@@ -2117,7 +2130,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
|
||||
'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
@@ -2185,6 +2198,9 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC address'
|
||||
|
||||
@@ -480,17 +480,23 @@ class CablePath(models.Model):
|
||||
|
||||
def get_total_length(self):
|
||||
"""
|
||||
Return the sum of the length of each cable in the path.
|
||||
Return a tuple containing the sum of the length of each cable in the path
|
||||
and a flag indicating whether the length is definitive.
|
||||
"""
|
||||
cable_ids = [
|
||||
# Starting from the first element, every third element in the path should be a Cable
|
||||
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
|
||||
]
|
||||
return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']
|
||||
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
||||
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
||||
is_definitive = len(cables) == len(cable_ids)
|
||||
|
||||
return total_length, is_definitive
|
||||
|
||||
def get_split_nodes(self):
|
||||
"""
|
||||
Return all available next segments in a split cable path.
|
||||
"""
|
||||
rearport = path_node_to_object(self.path[-1])
|
||||
|
||||
return FrontPort.objects.filter(rear_port=rearport)
|
||||
|
||||
@@ -198,7 +198,7 @@ class PathEndpoint(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
@@ -234,7 +234,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
@@ -270,7 +270,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
@@ -379,7 +379,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
@@ -479,7 +479,7 @@ class BaseInterface(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
@@ -624,7 +624,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class FrontPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@@ -687,7 +687,7 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class RearPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
@@ -740,7 +740,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'custom_links')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@@ -800,7 +800,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
@extras_features('export_templates', 'webhooks', 'custom_links')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from django.conf import settings
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
|
||||
@@ -127,11 +128,18 @@ class DeviceTable(BaseTable):
|
||||
verbose_name='Type',
|
||||
text=lambda record: record.device_type.display_name
|
||||
)
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip6', 'primary_ip4'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
if settings.PREFER_IPV4:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip4', 'primary_ip6'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
else:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip6', 'primary_ip4'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
primary_ip4 = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IPv4 Address'
|
||||
|
||||
@@ -1488,9 +1488,9 @@ class ConsolePortTestCase(TestCase):
|
||||
ConsoleServerPort.objects.bulk_create(console_server_ports)
|
||||
|
||||
console_ports = (
|
||||
ConsolePort(device=devices[0], name='Console Port 1', description='First'),
|
||||
ConsolePort(device=devices[1], name='Console Port 2', description='Second'),
|
||||
ConsolePort(device=devices[2], name='Console Port 3', description='Third'),
|
||||
ConsolePort(device=devices[0], name='Console Port 1', label='A', description='First'),
|
||||
ConsolePort(device=devices[1], name='Console Port 2', label='B', description='Second'),
|
||||
ConsolePort(device=devices[2], name='Console Port 3', label='C', description='Third'),
|
||||
)
|
||||
ConsolePort.objects.bulk_create(console_ports)
|
||||
|
||||
@@ -1507,6 +1507,10 @@ class ConsolePortTestCase(TestCase):
|
||||
params = {'name': ['Console Port 1', 'Console Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1584,9 +1588,9 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
ConsolePort.objects.bulk_create(console_ports)
|
||||
|
||||
console_server_ports = (
|
||||
ConsoleServerPort(device=devices[0], name='Console Server Port 1', description='First'),
|
||||
ConsoleServerPort(device=devices[1], name='Console Server Port 2', description='Second'),
|
||||
ConsoleServerPort(device=devices[2], name='Console Server Port 3', description='Third'),
|
||||
ConsoleServerPort(device=devices[0], name='Console Server Port 1', label='A', description='First'),
|
||||
ConsoleServerPort(device=devices[1], name='Console Server Port 2', label='B', description='Second'),
|
||||
ConsoleServerPort(device=devices[2], name='Console Server Port 3', label='C', description='Third'),
|
||||
)
|
||||
ConsoleServerPort.objects.bulk_create(console_server_ports)
|
||||
|
||||
@@ -1603,6 +1607,10 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
params = {'name': ['Console Server Port 1', 'Console Server Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1680,9 +1688,9 @@ class PowerPortTestCase(TestCase):
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
|
||||
power_ports = (
|
||||
PowerPort(device=devices[0], name='Power Port 1', maximum_draw=100, allocated_draw=50, description='First'),
|
||||
PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, description='Second'),
|
||||
PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, description='Third'),
|
||||
PowerPort(device=devices[0], name='Power Port 1', label='A', maximum_draw=100, allocated_draw=50, description='First'),
|
||||
PowerPort(device=devices[1], name='Power Port 2', label='B', maximum_draw=200, allocated_draw=100, description='Second'),
|
||||
PowerPort(device=devices[2], name='Power Port 3', label='C', maximum_draw=300, allocated_draw=150, description='Third'),
|
||||
)
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
|
||||
@@ -1699,6 +1707,10 @@ class PowerPortTestCase(TestCase):
|
||||
params = {'name': ['Power Port 1', 'Power Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1784,9 +1796,9 @@ class PowerOutletTestCase(TestCase):
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
|
||||
power_outlets = (
|
||||
PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'),
|
||||
PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'),
|
||||
PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'),
|
||||
PowerOutlet(device=devices[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'),
|
||||
PowerOutlet(device=devices[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'),
|
||||
PowerOutlet(device=devices[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'),
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
|
||||
@@ -1803,6 +1815,10 @@ class PowerOutletTestCase(TestCase):
|
||||
params = {'name': ['Power Outlet 1', 'Power Outlet 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1879,12 +1895,12 @@ class InterfaceTestCase(TestCase):
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
|
||||
Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
|
||||
Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
|
||||
Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||
Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||
Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
|
||||
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
|
||||
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
|
||||
Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
|
||||
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True),
|
||||
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
@@ -1901,6 +1917,10 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@@ -2016,12 +2036,12 @@ class FrontPortTestCase(TestCase):
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
|
||||
front_ports = (
|
||||
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
|
||||
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
|
||||
FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
|
||||
FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
|
||||
FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
|
||||
FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
|
||||
FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
|
||||
FrontPort(device=devices[3], name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
|
||||
@@ -2038,6 +2058,10 @@ class FrontPortTestCase(TestCase):
|
||||
params = {'name': ['Front Port 1', 'Front Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
# TODO: Test for multiple values
|
||||
params = {'type': PortTypeChoices.TYPE_8P8C}
|
||||
@@ -2108,12 +2132,12 @@ class RearPortTestCase(TestCase):
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
rear_ports = (
|
||||
RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
|
||||
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
|
||||
RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
|
||||
RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4),
|
||||
RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5),
|
||||
RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6),
|
||||
RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
|
||||
RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
|
||||
RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
|
||||
RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
|
||||
RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
|
||||
RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
|
||||
)
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
|
||||
@@ -2130,6 +2154,10 @@ class RearPortTestCase(TestCase):
|
||||
params = {'name': ['Rear Port 1', 'Rear Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
# TODO: Test for multiple values
|
||||
params = {'type': PortTypeChoices.TYPE_8P8C}
|
||||
@@ -2203,9 +2231,9 @@ class DeviceBayTestCase(TestCase):
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
device_bays = (
|
||||
DeviceBay(device=devices[0], name='Device Bay 1', description='First'),
|
||||
DeviceBay(device=devices[1], name='Device Bay 2', description='Second'),
|
||||
DeviceBay(device=devices[2], name='Device Bay 3', description='Third'),
|
||||
DeviceBay(device=devices[0], name='Device Bay 1', label='A', description='First'),
|
||||
DeviceBay(device=devices[1], name='Device Bay 2', label='B', description='Second'),
|
||||
DeviceBay(device=devices[2], name='Device Bay 3', label='C', description='Third'),
|
||||
)
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
|
||||
@@ -2217,6 +2245,10 @@ class DeviceBayTestCase(TestCase):
|
||||
params = {'name': ['Device Bay 1', 'Device Bay 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2283,9 +2315,9 @@ class InventoryItemTestCase(TestCase):
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
inventory_items = (
|
||||
InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
|
||||
InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
|
||||
InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
|
||||
InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
|
||||
InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
|
||||
InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
|
||||
)
|
||||
for i in inventory_items:
|
||||
i.save()
|
||||
@@ -2306,6 +2338,10 @@ class InventoryItemTestCase(TestCase):
|
||||
params = {'name': ['Inventory Item 1', 'Inventory Item 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_part_id(self):
|
||||
params = {'part_id': ['1001', '1002']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -2134,10 +2134,14 @@ class PathTraceView(generic.ObjectView):
|
||||
else:
|
||||
path = related_paths.first()
|
||||
|
||||
# Get the total length of the cable and whether the length is definitive (fully defined)
|
||||
total_length, is_definitive = path.get_total_length() if path else (None, False)
|
||||
|
||||
return {
|
||||
'path': path,
|
||||
'related_paths': related_paths,
|
||||
'total_length': path.get_total_length() if path else None,
|
||||
'total_length': total_length,
|
||||
'is_definitive': is_definitive
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from cacheops import invalidate_model
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
@@ -27,7 +28,7 @@ class Command(BaseCommand):
|
||||
app_label, model_name = name.split('.')
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
|
||||
f"Invalid format: {name}. Models must be specified in the form app_label.ModelName."
|
||||
)
|
||||
try:
|
||||
app_config = apps.get_app_config(app_label)
|
||||
@@ -36,13 +37,13 @@ class Command(BaseCommand):
|
||||
try:
|
||||
model = app_config.get_model(model_name)
|
||||
except LookupError:
|
||||
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
|
||||
raise CommandError(f"Unknown model: {app_label}.{model_name}")
|
||||
fields = [
|
||||
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
||||
]
|
||||
if not fields:
|
||||
raise CommandError(
|
||||
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
|
||||
f"Invalid model: {app_label}.{model_name} does not employ natural ordering"
|
||||
)
|
||||
models.append(
|
||||
(model, fields)
|
||||
@@ -67,7 +68,7 @@ class Command(BaseCommand):
|
||||
models = self._get_models(args)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Renaturalizing {} models.".format(len(models)))
|
||||
self.stdout.write(f"Renaturalizing {len(models)} models.")
|
||||
|
||||
for model, fields in models:
|
||||
for field in fields:
|
||||
@@ -78,7 +79,7 @@ class Command(BaseCommand):
|
||||
# Print the model and field name
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
|
||||
f"{model._meta.label}.{field.target_field} ({field.name})... ",
|
||||
ending='\n' if options['verbosity'] >= 2 else ''
|
||||
)
|
||||
self.stdout.flush()
|
||||
@@ -89,23 +90,26 @@ class Command(BaseCommand):
|
||||
naturalized_value = naturalize(value, max_length=field.max_length)
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
|
||||
self.stdout.write(f" {value} -> {naturalized_value}", ending='')
|
||||
self.stdout.flush()
|
||||
|
||||
# Update each unique field value in bulk
|
||||
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" ({})".format(changed))
|
||||
self.stdout.write(f" ({changed})")
|
||||
count += changed
|
||||
|
||||
# Print the total count of alterations for the field
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
|
||||
count, model._meta.verbose_name_plural, queryset.count()
|
||||
)))
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
|
||||
))
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS(str(count)))
|
||||
|
||||
# Invalidate cached queries
|
||||
invalidate_model(model)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
|
||||
@@ -192,7 +192,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
field_name='prefix',
|
||||
lookup_expr='family'
|
||||
)
|
||||
prefix = django_filters.CharFilter(
|
||||
prefix = MultiValueCharFilter(
|
||||
method='filter_prefix',
|
||||
label='Prefix',
|
||||
)
|
||||
@@ -304,13 +304,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_prefix(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
query_values = []
|
||||
for v in value:
|
||||
try:
|
||||
query_values.append(netaddr.IPNetwork(v))
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(prefix__in=query_values)
|
||||
|
||||
def search_within(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
|
||||
@@ -465,12 +465,14 @@ class PrefixCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
if data:
|
||||
|
||||
# Limit vlan queryset by assigned site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'),
|
||||
}
|
||||
self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
|
||||
# Limit VLAN queryset by assigned site and/or group (if specified)
|
||||
params = {}
|
||||
if data.get('site'):
|
||||
params[f"site__{self.fields['site'].to_field_name}"] = data.get('site')
|
||||
if data.get('vlan_group'):
|
||||
params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group')
|
||||
if params:
|
||||
self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
|
||||
@@ -22,7 +22,7 @@ PREFIX_LINK = """
|
||||
{% for i in record.parents|as_range %}
|
||||
<i class="mdi mdi-circle-small"></i>
|
||||
{% endfor %}
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||
"""
|
||||
|
||||
PREFIX_ROLE_LINK = """
|
||||
@@ -37,7 +37,7 @@ IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
{% else %}
|
||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
||||
{% endif %}
|
||||
@@ -50,8 +50,8 @@ IPADDRESS_ASSIGN_LINK = """
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
{% elif prefix.vrf %}
|
||||
{{ prefix.vrf }}
|
||||
{% elif object.vrf %}
|
||||
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
|
||||
{% else %}
|
||||
Global
|
||||
{% endif %}
|
||||
@@ -114,6 +114,8 @@ TENANT_LINK = """
|
||||
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
|
||||
{% elif record.vrf.tenant %}
|
||||
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
|
||||
{% elif object.tenant %}
|
||||
<a href="{% url 'tenancy:tenant' slug=object.tenant.slug %}" title="{{ object.tenant.description }}">{{ object.tenant }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
@@ -431,6 +433,9 @@ class InterfaceIPAddressTable(BaseTable):
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=IPAddress
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
|
||||
@@ -422,6 +422,11 @@ class PrefixTestCase(TestCase):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_prefix(self):
|
||||
prefixes = Prefix.objects.all()[:2]
|
||||
params = {'prefix': [prefixes[0].prefix, prefixes[1].prefix]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_pool(self):
|
||||
params = {'is_pool': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
|
||||
import_targets_table = tables.RouteTargetTable(
|
||||
instance.import_targets.prefetch_related('tenant'),
|
||||
@@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'prefix_count': prefix_count,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'import_targets_table': import_targets_table,
|
||||
'export_targets_table': export_targets_table,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
|
||||
from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from .routers import OrderedDefaultRouter
|
||||
from .serializers import BulkOperationSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
|
||||
@@ -9,7 +9,6 @@ __all__ = (
|
||||
'ContentTypeField',
|
||||
'OrderedDefaultRouter',
|
||||
'SerializedPKRelatedField',
|
||||
'TimeZoneField',
|
||||
'ValidatedModelSerializer',
|
||||
'WritableNestedSerializer',
|
||||
)
|
||||
|
||||
@@ -104,21 +104,6 @@ class ContentTypeField(RelatedField):
|
||||
return f"{obj.app_label}.{obj.model}"
|
||||
|
||||
|
||||
class TimeZoneField(serializers.Field):
|
||||
"""
|
||||
Represent a pytz time zone.
|
||||
"""
|
||||
def to_representation(self, obj):
|
||||
return obj.zone if obj else None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not data:
|
||||
return ""
|
||||
if data not in pytz.common_timezones:
|
||||
raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
|
||||
return pytz.timezone(data)
|
||||
|
||||
|
||||
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
||||
"""
|
||||
Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
|
||||
|
||||
@@ -154,6 +154,9 @@ LOGIN_TIMEOUT = None
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
# The URL to use when mapping physical addresses or GPS coordinates
|
||||
MAPS_URL = 'https://maps.google.com/?q='
|
||||
|
||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
||||
# all objects by specifying "?limit=0".
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.10.5'
|
||||
VERSION = '2.10.8'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -88,10 +88,9 @@ LOGGING = getattr(configuration, 'LOGGING', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
|
||||
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||
MAPS_URL = getattr(configuration, 'MAPS_URL', 'https://maps.google.com/?q=')
|
||||
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
|
||||
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
||||
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
|
||||
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
|
||||
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
|
||||
@@ -118,18 +117,23 @@ SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
|
||||
# Validate update repo URL and timeout
|
||||
if RELEASE_CHECK_URL:
|
||||
try:
|
||||
URLValidator(RELEASE_CHECK_URL)
|
||||
except ValidationError:
|
||||
raise ImproperlyConfigured(
|
||||
validator = URLValidator(
|
||||
message=(
|
||||
"RELEASE_CHECK_URL must be a valid API URL. Example: "
|
||||
"https://api.github.com/repos/netbox-community/netbox"
|
||||
)
|
||||
)
|
||||
try:
|
||||
validator(RELEASE_CHECK_URL)
|
||||
except ValidationError as err:
|
||||
raise ImproperlyConfigured(str(err))
|
||||
|
||||
# Enforce a minimum cache timeout for update checks
|
||||
if RELEASE_CHECK_TIMEOUT < 3600:
|
||||
|
||||
@@ -362,9 +362,6 @@ table.report th a {
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.banner-bottom {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.panel table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
{% block content %}{% endblock %}
|
||||
<div class="push"></div>
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||
<div class="alert alert-info text-center" style="margin-bottom: 50px" role="alert">
|
||||
{{ settings.BANNER_BOTTOM|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<h5>Total segments: {{ traced_path|length }}</h5>
|
||||
<h5>Total length:
|
||||
{% if total_length %}
|
||||
{{ total_length|floatformat:"-2" }} Meters /
|
||||
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters /
|
||||
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load custom_links %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block header %}
|
||||
@@ -30,6 +31,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ object.device }} / {{ object }}{% endblock %}</h1>
|
||||
<div class="pull-right noprint">
|
||||
{% custom_links object %}
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -226,7 +227,23 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Addresses</strong>
|
||||
</div>
|
||||
{% if ipaddress_table.rows %}
|
||||
{% render_table ipaddress_table 'inc/table.html' %}
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<td>
|
||||
{% if object.physical_address %}
|
||||
<div class="pull-right noprint">
|
||||
<a href="https://maps.google.com/?q={{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<a href="{{ settings.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<i class="mdi mdi-map-marker"></i> Map it
|
||||
</a>
|
||||
</div>
|
||||
@@ -156,7 +156,7 @@
|
||||
<td>
|
||||
{% if object.latitude and object.longitude %}
|
||||
<div class="pull-right noprint">
|
||||
<a href="https://maps.google.com/?q={{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<a href="{{ settings.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<i class="mdi mdi-map-marker"></i> Map it
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h1>{{ script }}</h1>
|
||||
<p>{{ script.Meta.description }}</p>
|
||||
<p>{{ script.Meta.description|render_markdown }}</p>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#run" role="tab" data-toggle="tab" class="active">Run</a>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<td>
|
||||
{% include 'extras/inc/job_label.html' with result=script.result %}
|
||||
</td>
|
||||
<td>{{ script.Meta.description }}</td>
|
||||
<td>{{ script.Meta.description|render_markdown }}</td>
|
||||
{% if script.result %}
|
||||
<td class="text-right">
|
||||
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created }}</a>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h1>{{ script }}</h1>
|
||||
<p>{{ script.Meta.description }}</p>
|
||||
<p>{{ script.Meta.description|render_markdown }}</p>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#log" role="tab" data-toggle="tab" class="active">Log</a>
|
||||
@@ -110,4 +110,4 @@ function jobTerminatedAction(){
|
||||
</script>
|
||||
<script src="{% static 'js/job_result.js' %}?v{{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=js/job_result.js'"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -304,13 +304,7 @@
|
||||
{% for change in changelog %}
|
||||
{% with action=change.get_action_display|lower %}
|
||||
<div class="list-group-item">
|
||||
{% if action == 'created' %}
|
||||
<span class="label label-success">Created</span>
|
||||
{% elif action == 'updated' %}
|
||||
<span class="label label-warning">Modified</span>
|
||||
{% elif action == 'deleted' %}
|
||||
<span class="label label-danger">Deleted</span>
|
||||
{% endif %}
|
||||
<span class="label label-{{ change.get_action_class }}">{{ change.get_action_display }}</span>
|
||||
{{ change.changed_object_type.name|bettertitle }}
|
||||
{% if change.changed_object.get_absolute_url %}
|
||||
<a href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>
|
||||
|
||||
@@ -95,6 +95,12 @@
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Addresses</td>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
@@ -93,7 +94,23 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Addresses</strong>
|
||||
</div>
|
||||
{% if ipaddress_table.rows %}
|
||||
{% render_table ipaddress_table 'inc/table.html' %}
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -223,7 +223,7 @@ class ObjectTypeListFilter(admin.SimpleListFilter):
|
||||
parameter_name = 'object_type'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
object_types = ObjectPermission.objects.values_list('id', flat=True).distinct()
|
||||
object_types = ObjectPermission.objects.values_list('object_types__pk', flat=True).distinct()
|
||||
content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
|
||||
return [
|
||||
(ct.pk, ct) for ct in content_types
|
||||
|
||||
@@ -215,7 +215,7 @@ class CommentField(forms.CharField):
|
||||
default_label = ''
|
||||
# TODO: Port Markdown cheat sheet to internal documentation
|
||||
default_helptext = '<i class="mdi mdi-information-outline"></i> '\
|
||||
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
|
||||
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank" tabindex="-1">'\
|
||||
'Markdown</a> syntax is supported'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from django.conf import settings
|
||||
from dcim.tables.devices import BaseInterfaceTable
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import (
|
||||
@@ -125,10 +125,18 @@ class VirtualMachineDetailTable(VirtualMachineTable):
|
||||
linkify=True,
|
||||
verbose_name='IPv6 Address'
|
||||
)
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
if settings.PREFER_IPV4:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip4', 'primary_ip6'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
else:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip6', 'primary_ip4'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='virtualization:virtualmachine_list'
|
||||
)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
Django==3.1.3
|
||||
Django==3.1.7
|
||||
django-cacheops==5.1
|
||||
django-cors-headers==3.5.0
|
||||
django-debug-toolbar==3.1.1
|
||||
django-cors-headers==3.7.0
|
||||
django-debug-toolbar==3.2
|
||||
django-filter==2.4.0
|
||||
django-mptt==0.11.0
|
||||
django-mptt==0.12.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.1.0
|
||||
django-rq==2.4.0
|
||||
django-tables2==2.3.3
|
||||
django-tables2==2.3.4
|
||||
django-taggit==1.3.0
|
||||
django-timezone-field==4.0
|
||||
djangorestframework==3.12.2
|
||||
django-timezone-field==4.1.2
|
||||
djangorestframework==3.12.4
|
||||
drf-yasg[validation]==1.20.0
|
||||
gunicorn==20.0.4
|
||||
Jinja2==2.11.2
|
||||
Markdown==3.3.3
|
||||
Jinja2==2.11.3
|
||||
Markdown==3.3.4
|
||||
netaddr==0.8.0
|
||||
Pillow==8.0.1
|
||||
Pillow==8.1.2
|
||||
psycopg2-binary==2.8.6
|
||||
pycryptodome==3.9.9
|
||||
PyYAML==5.3.1
|
||||
svgwrite==1.4
|
||||
pycryptodome==3.10.1
|
||||
PyYAML==5.4.1
|
||||
svgwrite==1.4.1
|
||||
|
||||
Reference in New Issue
Block a user