Compare commits
195 Commits
v4.1-beta1
...
v4.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead6e637f4 | ||
|
|
b4e181c015 | ||
|
|
2a8b11de8e | ||
|
|
dee8e6f733 | ||
|
|
e93719e4f9 | ||
|
|
52a0b454c0 | ||
|
|
0ec632e548 | ||
|
|
cc6f21ded2 | ||
|
|
b4dd71d578 | ||
|
|
9bccc11c28 | ||
|
|
5485b04bcd | ||
|
|
68bd37a08f | ||
|
|
85396866bc | ||
|
|
50df0a1073 | ||
|
|
01db481bc3 | ||
|
|
cfb5696d29 | ||
|
|
8420af8562 | ||
|
|
b7df06ae7f | ||
|
|
88abb6902d | ||
|
|
116a423d8f | ||
|
|
9c9c4fbd6e | ||
|
|
896b5b9b74 | ||
|
|
dd0774d8d4 | ||
|
|
b46a89640f | ||
|
|
6b219a279b | ||
|
|
2dd5c82845 | ||
|
|
f4d4483050 | ||
|
|
8deaaae44b | ||
|
|
6d0a3485a1 | ||
|
|
8e6a15a25e | ||
|
|
44c7786cd9 | ||
|
|
f7dd09da08 | ||
|
|
3000407a5f | ||
|
|
7167f20ca5 | ||
|
|
c79c664e96 | ||
|
|
f5d6f5d997 | ||
|
|
48ca5d4d7e | ||
|
|
893adef79c | ||
|
|
bdf182ffd6 | ||
|
|
65f81ab81d | ||
|
|
fa1e89d3d2 | ||
|
|
0e34fba922 | ||
|
|
15a106b663 | ||
|
|
18935687ae | ||
|
|
84ea437345 | ||
|
|
b3e4d3c346 | ||
|
|
5ce8b6b825 | ||
|
|
5486e9238d | ||
|
|
114b8a2860 | ||
|
|
52299faecd | ||
|
|
ee7ac6f041 | ||
|
|
c0d5d98df2 | ||
|
|
af5fb8f923 | ||
|
|
213eb610de | ||
|
|
5f32b23c35 | ||
|
|
bf7b8b2458 | ||
|
|
89604ceade | ||
|
|
ce372ce7a6 | ||
|
|
353db09656 | ||
|
|
ce38484bf3 | ||
|
|
81a1280b65 | ||
|
|
93eea95735 | ||
|
|
0c2861e02c | ||
|
|
43b949779d | ||
|
|
aca693b1c3 | ||
|
|
7ab2ebcb75 | ||
|
|
16f74f7b03 | ||
|
|
76ff329c6a | ||
|
|
ed1b8bc88d | ||
|
|
684cdda8f4 | ||
|
|
2a1710b82c | ||
|
|
7404caed3a | ||
|
|
5c33aa8bdd | ||
|
|
8cc0616019 | ||
|
|
886d635524 | ||
|
|
7bc0d34196 | ||
|
|
9cfb3bf24d | ||
|
|
8707c44a85 | ||
|
|
16dfafa35b | ||
|
|
39c5f57c1e | ||
|
|
7c0bdf3fb8 | ||
|
|
a777850702 | ||
|
|
0b120e6ad2 | ||
|
|
e174a8af09 | ||
|
|
77d1dc4807 | ||
|
|
2f1798c7de | ||
|
|
52b7f62b10 | ||
|
|
7a2ff96abe | ||
|
|
814b699204 | ||
|
|
ce33e0bc0c | ||
|
|
d18a853baa | ||
|
|
b2bbdbf1d9 | ||
|
|
56f110c2a9 | ||
|
|
31d5d8c395 | ||
|
|
00874ac9e7 | ||
|
|
4fead1c85f | ||
|
|
b4dd57f3c7 | ||
|
|
8fff4e2a5d | ||
|
|
6db29880cc | ||
|
|
829bef041d | ||
|
|
b797fcc03d | ||
|
|
af93b47f94 | ||
|
|
10a74492f6 | ||
|
|
a6b1f97525 | ||
|
|
a09c1667ff | ||
|
|
91ad3f22a3 | ||
|
|
cdf2fdee83 | ||
|
|
2f53dc0095 | ||
|
|
a9a926cc72 | ||
|
|
00d8eb40fc | ||
|
|
db6246a437 | ||
|
|
9a11191ac5 | ||
|
|
6f7bf5baf4 | ||
|
|
31f167d0f9 | ||
|
|
e93e9ac4a0 | ||
|
|
97cd6b89fd | ||
|
|
3684e011e6 | ||
|
|
a9fd5bbf55 | ||
|
|
e325a4b2e0 | ||
|
|
a150e5d561 | ||
|
|
8282a6ddfe | ||
|
|
07b1362b5e | ||
|
|
e3e351d1f0 | ||
|
|
50839fcb6b | ||
|
|
cac92352ca | ||
|
|
0464dacf7e | ||
|
|
cf62178471 | ||
|
|
c3276b7d2e | ||
|
|
7bae448eaf | ||
|
|
5ebdb7c9d2 | ||
|
|
0157ac6c9b | ||
|
|
d23b9370f6 | ||
|
|
c2d67fa17e | ||
|
|
4f225b4e56 | ||
|
|
263664ae52 | ||
|
|
0238aeec22 | ||
|
|
3fee28cd5e | ||
|
|
515d041560 | ||
|
|
8bea914163 | ||
|
|
420613daed | ||
|
|
fd013d6c5c | ||
|
|
a7f83de8c4 | ||
|
|
ee0af15073 | ||
|
|
35e2cf9cec | ||
|
|
1d2ea90fd4 | ||
|
|
dab27695b9 | ||
|
|
91df0c0686 | ||
|
|
d4dd86eb04 | ||
|
|
5bd4fc862d | ||
|
|
85f8364cd7 | ||
|
|
8903e4649c | ||
|
|
38a26a7908 | ||
|
|
fb6c7d7619 | ||
|
|
3412139acc | ||
|
|
96802b4edb | ||
|
|
e656e2da24 | ||
|
|
c457f01b19 | ||
|
|
8e70e3ad20 | ||
|
|
33e8e04281 | ||
|
|
1504fb5ac2 | ||
|
|
dd3166a4ed | ||
|
|
b6071a80d9 | ||
|
|
93a13ce311 | ||
|
|
122522a625 | ||
|
|
a3befd0abe | ||
|
|
51c3d94725 | ||
|
|
e3d681be54 | ||
|
|
234b4027b0 | ||
|
|
277b7039d8 | ||
|
|
e2dfa63df6 | ||
|
|
c6c0ab2828 | ||
|
|
8b91fb8d2d | ||
|
|
370c1209f4 | ||
|
|
09d6b9c62f | ||
|
|
4747cdef0b | ||
|
|
f3f1aa3841 | ||
|
|
727cb65c50 | ||
|
|
872af72b8e | ||
|
|
5a9f9af2fa | ||
|
|
09d36469dd | ||
|
|
8789aaaa39 | ||
|
|
d5c1a5acda | ||
|
|
6feb8bf0e3 | ||
|
|
9e54cfe340 | ||
|
|
6a663e2a3e | ||
|
|
7c9a77b77f | ||
|
|
81fe12a7d9 | ||
|
|
9c7002f691 | ||
|
|
20967bf88d | ||
|
|
34d20fccd5 | ||
|
|
f6c1642116 | ||
|
|
b7b0ab16f5 | ||
|
|
6ae3af2f26 | ||
|
|
6c845bd5de | ||
|
|
597fc926c0 |
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.8
|
||||
placeholder: v4.1.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -26,7 +26,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.8
|
||||
placeholder: v4.1.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
21
.github/workflows/auto-assign-issue.yml
vendored
@@ -1,21 +0,0 @@
|
||||
# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
|
||||
name: Issue assignment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v2
|
||||
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
|
||||
with:
|
||||
# Weighted assignments
|
||||
assignees: arthanson:3, jeremystretch:3, DanSheps
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
||||
5
.github/workflows/close-stale-issues.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# General parameters
|
||||
operations-per-run: 100
|
||||
operations-per-run: 200
|
||||
remove-stale-when-updated: false
|
||||
|
||||
# Issue parameters
|
||||
@@ -43,8 +43,9 @@ jobs:
|
||||
# Pull request parameters
|
||||
close-pr-message: >
|
||||
This PR has been automatically closed due to lack of activity.
|
||||
days-before-pr-stale: 15
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 15
|
||||
exempt-pr-labels: 'status: blocked'
|
||||
stale-pr-label: 'pending closure'
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had
|
||||
|
||||
@@ -40,7 +40,7 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
|
||||
|
||||
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
|
||||
|
||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up ( :thumbsup: ). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||
|
||||
* If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.)
|
||||
|
||||
@@ -56,7 +56,9 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
|
||||
## :bulb: Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
|
||||
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up ( :thumbsup: ). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
|
||||
|
||||
* Please don't submit duplicate issues! Sometimes we reject feature requests, for various reasons. Even if you disagree with those reasons, please **do not** submit a duplicate feature request. It is very disrepectful of the maintainers' time, and you may be barred from opening future issues.
|
||||
|
||||
* If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members.
|
||||
|
||||
|
||||
@@ -84,10 +84,6 @@ Jinja2
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
|
||||
# File inclusion plugin for Python-Markdown
|
||||
# https://github.com/cmacmackin/markdown-include
|
||||
markdown-include
|
||||
|
||||
# MkDocs Material theme (for documentation build)
|
||||
# https://squidfunk.github.io/mkdocs-material/changelog/
|
||||
mkdocs-material
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
Alias /static /opt/netbox/netbox/static
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
Options FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
"nema-l15-60p",
|
||||
"nema-l21-20p",
|
||||
"nema-l21-30p",
|
||||
"nema-l22-20p",
|
||||
"nema-l22-30p",
|
||||
"cs6361c",
|
||||
"cs6365c",
|
||||
@@ -262,6 +263,7 @@
|
||||
"nema-l15-60r",
|
||||
"nema-l21-20r",
|
||||
"nema-l21-30r",
|
||||
"nema-l22-20r",
|
||||
"nema-l22-30r",
|
||||
"CS6360C",
|
||||
"CS6364C",
|
||||
@@ -288,6 +290,7 @@
|
||||
"molex-micro-fit-2x2",
|
||||
"molex-micro-fit-2x4",
|
||||
"dc-terminal",
|
||||
"eaton-c39",
|
||||
"hdot-cx",
|
||||
"saf-d-grid",
|
||||
"neutrik-powercon-20a",
|
||||
@@ -328,6 +331,7 @@
|
||||
"5gbase-t",
|
||||
"10gbase-t",
|
||||
"10gbase-cx4",
|
||||
"100base-x-sfp",
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
@@ -377,7 +381,9 @@
|
||||
"ieee802.11ad",
|
||||
"ieee802.11ax",
|
||||
"ieee802.11ay",
|
||||
"ieee802.11be",
|
||||
"ieee802.15.1",
|
||||
"ieee802.15.4",
|
||||
"other-wireless",
|
||||
"gsm",
|
||||
"cdma",
|
||||
@@ -517,6 +523,14 @@
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
@@ -574,6 +588,14 @@
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
|
||||
18
docs/_theme/partials/copyright.html
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="md-copyright">
|
||||
{% if config.copyright %}
|
||||
<div class="md-copyright__highlight">
|
||||
{{ config.copyright }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not config.extra.generator == false %}
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not config.extra.build_public %}
|
||||
<div class="md-copyright">
|
||||
ℹ️ Documentation is being served locally
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -5,7 +5,7 @@
|
||||
Default: False
|
||||
|
||||
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
|
||||
clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
|
||||
clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
|
||||
interface.
|
||||
|
||||
!!! warning
|
||||
|
||||
17
docs/configuration/graphql-api.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# GraphQL API Parameters
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_MAX_ALIASES
|
||||
|
||||
Default: 10
|
||||
|
||||
The maximum number of queries that a GraphQL API request may contain.
|
||||
@@ -25,7 +25,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
||||
* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators)
|
||||
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
|
||||
* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique)
|
||||
* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled)
|
||||
* [`GRAPHQL_ENABLED`](./graphql-api.md#graphql_enabled)
|
||||
* [`JOB_RETENTION`](./miscellaneous.md#job_retention)
|
||||
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
|
||||
* [`MAPS_URL`](./miscellaneous.md#maps_url)
|
||||
|
||||
@@ -122,16 +122,6 @@ The maximum amount (in bytes) of uploaded data that will be held in memory befor
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## JOB_RETENTION
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
@@ -20,19 +20,29 @@ A list of permitted URL schemes referenced when rendering links within NetBox. N
|
||||
|
||||
## AUTH_PASSWORD_VALIDATORS
|
||||
|
||||
This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
|
||||
This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. These rules are applied whenever a user's password is created or updated to ensure that it meets minimum criteria such as length or complexity. The default configuration is shown below.
|
||||
|
||||
```python
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
'OPTIONS': {
|
||||
'min_length': 10,
|
||||
}
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"min_length": 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
"NAME": "utilities.password_validation.AlphanumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
The default configuration enforces the follow criteria:
|
||||
|
||||
* A password must be at least 12 characters in length.
|
||||
* A password must have at least one uppercase letter, one lowercase letter, and one numeric digit.
|
||||
|
||||
Although it is not recommended, the default validation rules can be disabled by setting `AUTH_PASSWORD_VALIDATORS = []` in the configuration file. For more detail on customizing password validation, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
||||
@@ -83,12 +83,14 @@ Default: `('127.0.0.1', '::1')`
|
||||
|
||||
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
|
||||
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
||||
addresses (and [`DEBUG`](#debug) is true).
|
||||
addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
---
|
||||
|
||||
## ISOLATED_DEPLOYMENT
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
Default: False
|
||||
|
||||
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
@@ -117,7 +119,7 @@ JINJA2_FILTERS = {
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins).
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](./development.md#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](./miscellaneous.md#admins).
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file:
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ They can also be used as a mechanism for validating the integrity of data within
|
||||
|
||||
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||
|
||||
!!! danger "Only install trusted scripts"
|
||||
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
||||
|
||||
## Writing Custom Scripts
|
||||
|
||||
All custom scripts must inherit from the `extras.scripts.Script` base class. This class provides the functionality necessary to generate forms and log activity.
|
||||
|
||||
@@ -71,7 +71,6 @@ Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
|
||||
Create the following for each model:
|
||||
|
||||
* Detailed (full) model serializer in `api/serializers.py`
|
||||
* Nested serializer in `api/nested_serializers.py`
|
||||
* API view in `api/views.py`
|
||||
* Endpoint route in `api/urls.py`
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
|
||||
|
||||
## 5. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field.
|
||||
|
||||
## 6. Add fields to forms
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
|
||||
* Major release (e.g. v2.11 to v3.0)
|
||||
* Minor release (e.g. v3.2 to v3.3)
|
||||
* Patch release (e.g. v3.3.0 to v3.3.1)
|
||||
* Major release (e.g. v3.7.8 to v4.0.0)
|
||||
* Minor release (e.g. v4.0.10 to v4.1.0)
|
||||
* Patch release (e.g. v4.1.0 to v4.1.1)
|
||||
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
@@ -19,7 +19,7 @@ Sometimes it becomes necessary to constrain dependencies to a particular version
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-python-dependencies) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
|
||||
### Close the Release Milestone
|
||||
|
||||
@@ -39,6 +39,10 @@ mkdocs serve
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Test Upgrade Paths
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
@@ -90,7 +94,7 @@ Updated language translations should be pulled from [Transifex](https://app.tran
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the `VERSION` constant in `settings.py` to the new release version.
|
||||
* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
@@ -117,16 +121,6 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
|
||||
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
### Update the Development Version
|
||||
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
|
||||
|
||||
```
|
||||
VERSION = 'v3.3.2-dev'
|
||||
```
|
||||
|
||||
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
|
||||
|
||||
### Update the Public Documentation
|
||||
|
||||
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
|
||||
|
||||
@@ -41,7 +41,7 @@ Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
@@ -112,4 +112,4 @@ Authorization: Token $TOKEN
|
||||
|
||||
## Disabling the GraphQL API
|
||||
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/miscellaneous.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/graphql-api.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
|
||||
@@ -101,7 +101,7 @@ See the [filtering documentation](../reference/filtering.md) for more details on
|
||||
|
||||
## Serialization
|
||||
|
||||
The REST API employs two types of serializers to represent model data: base serializers and nested serializers. The base serializer is used to present the complete view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
|
||||
The REST API generally represents objects in one of two ways: complete or brief. The base serializer is used to present the complete view of an object. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes. Serializers employ a minimal "brief" representation of related objects, which includes only the attributes prudent for identifying the object.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -139,7 +139,7 @@ The REST API employs two types of serializers to represent model data: base seri
|
||||
|
||||
### Related Objects
|
||||
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using nested serializers. A nested serializer provides a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
|
||||
Related objects (e.g. `ForeignKey` fields) are included using nested brief representations. This is a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
|
||||
|
||||
For example, when creating a new device, its rack can be specified by NetBox ID (PK):
|
||||
|
||||
@@ -151,7 +151,7 @@ For example, when creating a new device, its rack can be specified by NetBox ID
|
||||
}
|
||||
```
|
||||
|
||||
Or by a set of nested attributes which uniquely identify the rack:
|
||||
Or by a set of attributes which uniquely identify the rack:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 548 KiB |
|
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 562 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 372 KiB |
@@ -16,6 +16,8 @@ The device to which this module bay belongs.
|
||||
|
||||
### Module
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.1."
|
||||
|
||||
The module to which this bay belongs (optional).
|
||||
|
||||
### Name
|
||||
|
||||
@@ -59,8 +59,3 @@ The maximum total weight capacity for all installed devices, inclusive of the ra
|
||||
### Descending Units
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
|
||||
### Airflow
|
||||
|
||||
The direction in which air circulates through the rack for cooling.
|
||||
|
||||
|
||||
@@ -57,7 +57,11 @@ A numeric weight used to override alphabetic ordering of fields by name. Custom
|
||||
|
||||
### Required
|
||||
|
||||
If checked, this custom field must be populated with a valid value for the object to pass validation.
|
||||
If enabled, this custom field must be populated with a valid value for the object to pass validation.
|
||||
|
||||
### Unique
|
||||
|
||||
If enabled, each object must have a unique value set for this custom field (per object type).
|
||||
|
||||
### Description
|
||||
|
||||
@@ -116,7 +120,3 @@ For numeric custom fields only. The maximum valid value (optional).
|
||||
### Validation Regex
|
||||
|
||||
For string-based custom fields only. A regular expression used to validate the field's value (optional).
|
||||
|
||||
### Uniqueness Validation
|
||||
|
||||
If enabled, each object must have a unique value set for this custom field (per object type).
|
||||
|
||||
@@ -10,4 +10,4 @@ A human-friendly name that is unique to the assigned virtual machine.
|
||||
|
||||
### Size
|
||||
|
||||
The allocated disk size, in gigabytes.
|
||||
The allocated disk size, in megabytes.
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,21 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 320">
|
||||
<g fill="#9cc8f8" stroke="#9cc8f8">
|
||||
<circle cx="37" cy="284" r="23"/>
|
||||
<circle cx="101" cy="37" r="23"/>
|
||||
<circle cx="101" cy="220" r="23"/>
|
||||
<circle cx="284" cy="220" r="23"/>
|
||||
<rect x="93" y="37" width="16" height="180"/>
|
||||
<rect x="101" y="212" width="180" height="16"/>
|
||||
<rect x="93" y="212" width="16" height="90" transform="rotate(45 101 220)"/>
|
||||
</g>
|
||||
<g fill="#1685fc" stroke="#1685fc">
|
||||
<circle cx="284" cy="37" r="23"/>
|
||||
<circle cx="37" cy="101" r="23"/>
|
||||
<circle cx="220" cy="101" r="23"/>
|
||||
<circle cx="220" cy="284" r="23"/>
|
||||
<rect x="37" y="93" width="180" height="16"/>
|
||||
<rect x="212" y="101" width="16" height="180"/>
|
||||
<rect x="212" y="93" width="16" height="90" transform="rotate(225 220 101)"/>
|
||||
<path transform="translate(380, 8)" d="M13.60 200L13.60 104L36.40 104L36.40 119.40L36.80 119.40Q40.20 112.20 47.20 106.90Q54.20 101.60 66.20 101.60L66.20 101.60Q75.80 101.60 82.50 104.80Q89.20 108 93.40 113.20Q97.60 118.40 99.40 125.20Q101.20 132 101.20 139.40L101.20 139.40L101.20 200L77.20 200L77.20 151.40Q77.20 147.40 76.80 142.50Q76.40 137.60 74.70 133.30Q73 129 69.40 126.10Q65.80 123.20 59.60 123.20L59.60 123.20Q53.60 123.20 49.50 125.20Q45.40 127.20 42.70 130.60Q40 134 38.80 138.40Q37.60 142.80 37.60 147.60L37.60 147.60L37.60 200L13.60 200ZM224.80 160.40L151.60 160.40Q152.80 171.20 160 177.20Q167.20 183.20 177.40 183.20L177.40 183.20Q186.40 183.20 192.50 179.50Q198.60 175.80 203.20 170.20L203.20 170.20L220.40 183.20Q212 193.60 201.60 198Q191.20 202.40 179.80 202.40L179.80 202.40Q169 202.40 159.40 198.80Q149.80 195.20 142.80 188.60Q135.80 182 131.70 172.70Q127.60 163.40 127.60 152L127.60 152Q127.60 140.60 131.70 131.30Q135.80 122 142.80 115.40Q149.80 108.80 159.40 105.20Q169 101.60 179.80 101.60L179.80 101.60Q189.80 101.60 198.10 105.10Q206.40 108.60 212.30 115.20Q218.20 121.80 221.50 131.50Q224.80 141.20 224.80 153.80L224.80 153.80L224.80 160.40ZM151.60 142.40L200.80 142.40Q200.60 131.80 194.20 125.70Q187.80 119.60 176.40 119.60L176.40 119.60Q165.60 119.60 159.30 125.80Q153 132 151.60 142.40L151.60 142.40ZM259.80 124.40L240.00 124.40L240.00 104L259.80 104L259.80 76.20L283.80 76.20L283.80 104L310.20 104L310.20 124.40L283.80 124.40L283.80 166.40Q283.80 173.60 286.50 177.80Q289.20 182 297.20 182L297.20 182Q300.40 182 304.20 181.30Q308 180.60 310.20 179L310.20 179L310.20 199.20Q306.40 201 300.90 201.70Q295.40 202.40 291.20 202.40L291.20 202.40Q281.60 202.40 275.50 200.30Q269.40 198.20 265.90 193.90Q262.40 189.60 261.10 183.20Q259.80 176.80 259.80 168.40L259.80 168.40L259.80 124.40ZM333.20 200L333.20 48.80L357.20 48.80L357.20 116.20L357.80 116.20Q359.60 113.80 362.40 111.30Q365.20 108.80 369.20 106.60Q373.20 104.40 378.40 103Q383.60 101.60 390.40 101.60L390.40 101.60Q400.60 101.60 409.20 105.50Q417.80 109.40 423.90 116.20Q430 123 433.40 132.20Q436.80 141.40 436.80 152L436.80 152Q436.80 162.60 433.60 171.80Q430.40 181 424.20 187.80Q418 194.60 409.20 198.50Q400.40 202.40 389.40 202.40L389.40 202.40Q379.20 202.40 370.40 198.40Q361.60 194.40 356.40 185.60L356.40 185.60L356 185.60L356 200L333.20 200ZM412.80 152L412.80 152Q412.80 146.40 410.90 141.20Q409 136 405.30 132Q401.60 128 396.40 125.60Q391.20 123.20 384.60 123.20L384.60 123.20Q378 123.20 372.80 125.60Q367.60 128 363.90 132Q360.20 136 358.30 141.20Q356.40 146.40 356.40 152L356.40 152Q356.40 157.60 358.30 162.80Q360.20 168 363.90 172Q367.60 176 372.80 178.40Q378 180.80 384.60 180.80L384.60 180.80Q391.20 180.80 396.40 178.40Q401.60 176 405.30 172Q409 168 410.90 162.80Q412.80 157.60 412.80 152ZM458.40 152L458.40 152Q458.40 140.60 462.50 131.30Q466.60 122 473.60 115.40Q480.60 108.80 490.20 105.20Q499.80 101.60 510.60 101.60L510.60 101.60Q521.40 101.60 531 105.20Q540.60 108.80 547.60 115.40Q554.60 122 558.70 131.30Q562.80 140.60 562.80 152L562.80 152Q562.80 163.40 558.70 172.70Q554.60 182 547.60 188.60Q540.60 195.20 531 198.80Q521.40 202.40 510.60 202.40L510.60 202.40Q499.80 202.40 490.20 198.80Q480.60 195.20 473.60 188.60Q466.60 182 462.50 172.70Q458.40 163.40 458.40 152ZM482.40 152L482.40 152Q482.40 157.60 484.30 162.80Q486.20 168 489.90 172Q493.60 176 498.80 178.40Q504 180.80 510.60 180.80L510.60 180.80Q517.20 180.80 522.40 178.40Q527.60 176 531.30 172Q535 168 536.90 162.80Q538.80 157.60 538.80 152L538.80 152Q538.80 146.40 536.90 141.20Q535 136 531.30 132Q527.60 128 522.40 125.60Q517.20 123.20 510.60 123.20L510.60 123.20Q504 123.20 498.80 125.60Q493.60 128 489.90 132Q486.20 136 484.30 141.20Q482.40 146.40 482.40 152ZM575.40 200L614 148.40L580.80 104L610 104L629.20 132.80L650 104L677.40 104L644.60 148.40L683.20 200L654 200L629 165.60L603.80 200L575.40 200Z"/>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1299.6 366">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #00857d;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #001423;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-2" d="M337.27,228.59c-12.35,0-22.88,7.8-26.94,18.74h-174.71c-2.9-7.83-9.12-14.04-16.95-16.95V55.67c10.94-4.06,18.74-14.59,18.74-26.94,0-15.87-12.86-28.73-28.73-28.73s-28.73,12.86-28.73,28.73c0,12.35,7.8,22.88,18.74,26.94v174.71c-10.94,4.06-18.74,14.59-18.74,26.94,0,4.28.94,8.33,2.62,11.98l-41.85,41.85c-3.65-1.68-7.7-2.62-11.98-2.62-15.87,0-28.73,12.86-28.73,28.73s12.86,28.73,28.73,28.73,28.73-12.86,28.73-28.73c0-4.28-.94-8.33-2.62-11.98l41.85-41.85c3.65,1.68,7.7,2.62,11.98,2.62,12.35,0,22.88-7.8,26.94-18.74h174.71c4.06,10.94,14.59,18.74,26.94,18.74,15.87,0,28.73-12.86,28.73-28.73s-12.86-28.73-28.73-28.73Z"/>
|
||||
<path class="cls-1" d="M366,28.73c0,15.87-12.86,28.73-28.73,28.73-4.28,0-8.33-.94-11.98-2.62l-41.85,41.85c1.68,3.65,2.62,7.7,2.62,11.98,0,12.35-7.8,22.88-18.74,26.94v174.71c10.94,4.06,18.74,14.59,18.74,26.94,0,15.87-12.86,28.73-28.73,28.73s-28.73-12.86-28.73-28.73c0-12.35,7.8-22.88,18.74-26.94v-174.71c-7.83-2.9-14.04-9.12-16.95-16.95H55.67c-4.06,10.94-14.59,18.74-26.94,18.74-15.87,0-28.73-12.86-28.73-28.73s12.86-28.73,28.73-28.73c12.35,0,22.88,7.8,26.94,18.74h174.71c4.06-10.94,14.59-18.74,26.94-18.74,4.28,0,8.33.94,11.98,2.62l41.85-41.85c-1.68-3.65-2.62-7.7-2.62-11.98,0-15.87,12.86-28.73,28.73-28.73s28.73,12.86,28.73,28.73ZM579.76,136.45c-4.63-4.38-10.18-7.68-16.24-9.66-6.09-2.07-12.48-3.11-18.91-3.08-9.75-.17-19.37,2.17-27.95,6.78-2.68,1.56-5.23,3.35-7.61,5.34v-9.04h-34.53v134.64h34.53v-69.06c-.08-5.7.68-11.38,2.26-16.86,1.26-4.03,3.36-7.74,6.17-10.89,2.41-2.69,5.44-4.74,8.84-5.96,3.71-1.26,7.6-1.89,11.51-1.85,2.99,0,5.97.41,8.84,1.23,2.62.91,5,2.38,6.99,4.32,2.11,2.28,3.78,4.93,4.93,7.81,1.32,4.12,1.95,8.42,1.85,12.74v78.52h34.53v-85.1c.22-7.94-1.18-15.84-4.11-23.23-2.37-6.33-6.16-12.03-11.1-16.65ZM744.41,169.34c2.28,8.16,3.46,16.6,3.49,25.08v13.77h-98.46c.38,2.33,1.22,4.57,2.47,6.58,1.83,3.77,4.51,7.08,7.81,9.66,3.42,2.8,7.32,4.96,11.51,6.37,4.42,1.57,9.08,2.33,13.77,2.26,5.63.24,11.21-1.19,16.03-4.11,5.19-3.31,9.78-7.48,13.57-12.33l3.49-4.11,26.31,20.14-3.29,4.52c-14.18,18.09-34.12,27.34-59.2,27.34-9.78.09-19.49-1.72-28.57-5.34-8.34-3.34-15.84-8.46-21.99-15.01-6.02-6.49-10.7-14.1-13.77-22.4-3.18-8.83-4.78-18.16-4.73-27.54-.02-9.49,1.72-18.9,5.14-27.75,3.36-8.35,8.32-15.96,14.59-22.4,6.24-6.44,13.72-11.54,21.99-15.01,8.74-3.58,18.1-5.4,27.54-5.34,11.92,0,21.99,2.06,30.42,6.37,7.92,3.9,14.87,9.52,20.35,16.44,5.36,6.74,9.28,14.5,11.51,22.82ZM711.31,178.39c-.43-2.36-.98-4.69-1.64-6.99-1.14-3.45-3.04-6.61-5.55-9.25-2.45-2.78-5.56-4.9-9.04-6.17-8.68-3.42-18.36-3.27-26.93.41-3.87,1.69-7.37,4.13-10.28,7.19-2.81,2.83-5.05,6.18-6.58,9.87-.73,1.58-1.28,3.23-1.64,4.93h61.66ZM827.24,230.8c-2.56.57-5.18.84-7.81.82-2.41.12-4.82-.37-6.99-1.44-1.42-1.08-2.55-2.49-3.29-4.11-.93-2.36-1.42-4.87-1.44-7.4-.21-3.29-.41-6.58-.41-9.87v-50.57h33.71v-31.45h-33.71v-34.53h-34.53v34.53h-21.79v31.45h21.79v58.79c-.04,5.15.24,10.3.82,15.42.38,5.56,1.99,10.97,4.73,15.83,3.21,5.18,7.85,9.32,13.36,11.92,5.76,2.88,13.36,4.32,23.43,4.32,3.71-.04,7.42-.31,11.1-.82,4.47-.56,8.79-1.95,12.74-4.11l2.88-1.44v-34.33l-8.43,4.93c-1.93,1.02-4.01,1.72-6.17,2.06ZM997.03,166.46c3.16,8.91,4.76,18.3,4.73,27.75.04,9.32-1.56,18.57-4.73,27.34-3.07,8.3-7.75,15.92-13.77,22.4-6.1,6.56-13.53,11.74-21.79,15.21-8.94,3.62-18.51,5.44-28.16,5.34-9.17-.04-18.22-2.07-26.52-5.96-4.12-1.71-7.93-4.07-11.31-6.99v9.87h-34.53V53.41h34.53v83.04c3.23-2.59,6.75-4.8,10.48-6.58,8.54-4.07,17.88-6.18,27.34-6.17,9.65-.09,19.22,1.72,28.16,5.34,8.18,3.52,15.58,8.62,21.79,15.01,5.91,6.58,10.57,14.17,13.77,22.4ZM963.11,178.8c-1.41-4.39-3.8-8.39-6.99-11.72-3.07-3.26-6.78-5.85-10.89-7.61-9.47-3.57-19.92-3.57-29.39,0-4.12,1.76-7.83,4.35-10.89,7.61-3.12,3.37-5.5,7.37-6.99,11.72-1.71,4.96-2.55,10.17-2.47,15.42-.05,5.24.78,10.45,2.47,15.42,1.54,4.27,3.91,8.18,6.99,11.51,3.01,3.32,6.74,5.92,10.89,7.61,9.42,3.83,19.97,3.83,29.39,0,4.16-1.68,7.88-4.28,10.89-7.61,3.15-3.28,5.54-7.21,6.99-11.51,1.68-4.96,2.52-10.18,2.47-15.42.07-5.24-.77-10.46-2.47-15.42ZM1136.6,244.16c-28.24,27.15-72.89,27.15-101.13,0-13.17-13.29-20.56-31.24-20.55-49.95-.1-28.4,16.95-54.05,43.17-64.95,17.9-7.4,38.01-7.4,55.91,0,26.14,11,43.15,36.59,43.17,64.95,0,18.71-7.38,36.66-20.55,49.95ZM1118.51,178.8c-1.42-4.34-3.73-8.33-6.78-11.72-3.1-3.22-6.8-5.8-10.89-7.61-9.55-3.56-20.05-3.56-29.6,0-4.09,1.81-7.79,4.39-10.89,7.61-3.05,3.39-5.36,7.38-6.78,11.72-1.88,4.92-2.79,10.15-2.67,15.42-.08,5.26.82,10.49,2.67,15.42,1.47,4.25,3.77,8.17,6.78,11.51,3.05,3.28,6.77,5.87,10.89,7.61,9.49,3.84,20.11,3.84,29.6,0,4.13-1.74,7.84-4.33,10.89-7.61,3.01-3.34,5.32-7.26,6.78-11.51,1.75-4.95,2.66-10.16,2.67-15.42,0-5.25-.9-10.47-2.67-15.42ZM1291.58,126.79h-42.34l-26.52,39.47-26.93-39.47h-44.4l48.1,63.1-54.27,71.53h42.96l33.5-47.69,33.71,47.69h44.19l-54.27-71.53,46.25-63.1Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -25,6 +25,8 @@ project-name/
|
||||
|
||||
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
|
||||
|
||||
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
|
||||
|
||||
#### Example
|
||||
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||
@@ -41,31 +43,7 @@ class MyModelSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'foo', 'bar', 'baz')
|
||||
```
|
||||
|
||||
### Nested Serializers
|
||||
|
||||
There are two cases where it is generally desirable to show only a minimal representation of an object:
|
||||
|
||||
1. When displaying an object related to the one being viewed (for example, the region to which a site is assigned)
|
||||
2. Listing many objects using "brief" mode
|
||||
|
||||
To accommodate these, it is recommended to create nested serializers accompanying the "full" serializer for each model. NetBox provides the `WritableNestedSerializer` class for just this purpose. This class accepts a primary key value on write, but displays an object representation for read requests. It also includes a read-only `display` attribute which conveys the string representation of the object.
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
from rest_framework import serializers
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class NestedMyModelSerializer(WritableNestedSerializer):
|
||||
foo = SiteSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'display', 'foo')
|
||||
brief_fields = ('id', 'url', 'display', 'bar')
|
||||
```
|
||||
|
||||
## Viewsets
|
||||
|
||||
@@ -1,6 +1,69 @@
|
||||
# NetBox v4.0
|
||||
|
||||
## v4.0.9 (FUTURE)
|
||||
## v4.0.11 (2024-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17310](https://github.com/netbox-community/netbox/issues/17310) - Enforce restricted queryset for related objects in GraphQL API requests
|
||||
* [#17321](https://github.com/netbox-community/netbox/issues/17321) - Ensure the job is attributed to the specified user when using the `runscript` management command
|
||||
* [#17323](https://github.com/netbox-community/netbox/issues/17323) - Associate job with script object when executed using the `runscript` management command
|
||||
* [#17337](https://github.com/netbox-community/netbox/issues/17337) - Fix ordering of virtual device contexts by device name
|
||||
* [#17341](https://github.com/netbox-community/netbox/issues/17341) - Avoid `NoReverseMatch` exceptions with specific dashboard widget configurations
|
||||
|
||||
## v4.0.10 (2024-08-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16857](https://github.com/netbox-community/netbox/issues/16857) - Scroll long rendered Markdown content within tables
|
||||
* [#16905](https://github.com/netbox-community/netbox/issues/16905) - Enable filtering of device components by device status
|
||||
* [#16949](https://github.com/netbox-community/netbox/issues/16949) - Add device count column to sites table
|
||||
* [#17072](https://github.com/netbox-community/netbox/issues/17072) - Linkify email addresses & phone numbers in contact assignments list
|
||||
* [#17177](https://github.com/netbox-community/netbox/issues/17177) - Add facility field to locations filter form
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16292](https://github.com/netbox-community/netbox/issues/16292) - Ensure consistent evaluation of queryset for both individual and list GraphQL API queries
|
||||
* [#16385](https://github.com/netbox-community/netbox/issues/16385) - Restore support for white, gray, and black background colors
|
||||
* [#16640](https://github.com/netbox-community/netbox/issues/16640) - Fix potential corruption of JSON values in custom fields that are not UI-editable
|
||||
* [#16670](https://github.com/netbox-community/netbox/issues/16670) - Fix conflicts within OpenAPI schema definition regarding nested serializers
|
||||
* [#16733](https://github.com/netbox-community/netbox/issues/16733) - Fix bulk edit/delete of objects when using "select all" widget
|
||||
* [#16756](https://github.com/netbox-community/netbox/issues/16756) - Fix dynamic pagination of custom script results table
|
||||
* [#16825](https://github.com/netbox-community/netbox/issues/16825) - Avoid `NoReverseMatch` exception when displaying count of related object type with no list view
|
||||
* [#16946](https://github.com/netbox-community/netbox/issues/16946) - GraphQL API requests with an invalid filter should return an empty set
|
||||
* [#16959](https://github.com/netbox-community/netbox/issues/16959) - Fix function of "reset" button on objects filter form
|
||||
* [#16973](https://github.com/netbox-community/netbox/issues/16973) - Fix support for evaluating user token (`$user`) against custom field values in permission constraints
|
||||
* [#17007](https://github.com/netbox-community/netbox/issues/17007) - Center SSO authentication icon when backend is unnamed
|
||||
* [#17070](https://github.com/netbox-community/netbox/issues/17070) - Image height & width values should not be required when creating an image attachment via the REST API
|
||||
* [#17108](https://github.com/netbox-community/netbox/issues/17108) - Ensure template date & time filters always return localtime-aware values
|
||||
* [#17117](https://github.com/netbox-community/netbox/issues/17117) - Work around Safari rendering bug
|
||||
* [#17186](https://github.com/netbox-community/netbox/issues/17186) - Fix display of custom links with default style under dark mode
|
||||
* [#17219](https://github.com/netbox-community/netbox/issues/17219) - Fix system config view exception when custom validator classes are employed
|
||||
* [#17230](https://github.com/netbox-community/netbox/issues/17230) - Ensure consistent rendering for all dashboard widget colors
|
||||
* [#17256](https://github.com/netbox-community/netbox/issues/17256) - Fix VLAN group scope selection for non-English languages
|
||||
* [#17278](https://github.com/netbox-community/netbox/issues/17278) - Ensure hierarchy is recalculated when bulk editing recursively nested object types (e.g. tenant groups)
|
||||
* [#17279](https://github.com/netbox-community/netbox/issues/17279) - Do not regenerate key when updating a token via REST API
|
||||
* [#17286](https://github.com/netbox-community/netbox/issues/17286) - Fix exception when adding member device to virtual chassis via web UI
|
||||
|
||||
---
|
||||
|
||||
## v4.0.9 (2024-08-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16692](https://github.com/netbox-community/netbox/issues/16692) - Enable modifying VLAN assignment while bulk editing prefixes
|
||||
* [#17006](https://github.com/netbox-community/netbox/issues/17006) - Add IEEE 802.11be interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13459](https://github.com/netbox-community/netbox/issues/13459) - Correct OpenAPI schema type for `TreeNodeMultipleChoiceFilter`
|
||||
* [#16073](https://github.com/netbox-community/netbox/issues/16073) - Respect default values for custom fields during bulk import of objects
|
||||
* [#16176](https://github.com/netbox-community/netbox/issues/16176) - Restore ability to select multiple terminating devices when connecting a cable
|
||||
* [#16871](https://github.com/netbox-community/netbox/issues/16871) - Sanitize device ID query parameter when bulk editing components to prevent exception
|
||||
* [#17038](https://github.com/netbox-community/netbox/issues/17038) - Fix AttributeError exception when attempting to export system status data
|
||||
* [#17064](https://github.com/netbox-community/netbox/issues/17064) - Fix misaligned text within rendered Markdown code blocks
|
||||
* [#17124](https://github.com/netbox-community/netbox/issues/17124) - `BaseTable` should follow reverse one-to-one relationships when prefetching related objects
|
||||
* [#17131](https://github.com/netbox-community/netbox/issues/17131) - Fix exception when creating object-type custom field without selecting related object type
|
||||
* [#17144](https://github.com/netbox-community/netbox/issues/17144) - Avoid showing duplicated pop-up messages
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,18 +1,77 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1-beta1 (2024-08-05)
|
||||
## v4.1.2 (2024-09-26)
|
||||
|
||||
!!! danger "Not for Production Use"
|
||||
This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
|
||||
### Enhancements
|
||||
|
||||
* [#14201](https://github.com/netbox-community/netbox/issues/14201) - Enable global search for AS numbers using "AS" prefix
|
||||
* [#15408](https://github.com/netbox-community/netbox/issues/15408) - Enable bulk import of primary IPv4 & IPv6 addresses for virtual device contexts (VDCs)
|
||||
* [#16781](https://github.com/netbox-community/netbox/issues/16781) - Add 100Base-X SFP interface type
|
||||
* [#17255](https://github.com/netbox-community/netbox/issues/17255) - Include return URL when creating new IP address from prefix IPs list
|
||||
* [#17471](https://github.com/netbox-community/netbox/issues/17471) - Add Eaton C39 power outlet type
|
||||
* [#17482](https://github.com/netbox-community/netbox/issues/17482) - Do not preload Branch & StagedChange models in `nbshell`
|
||||
* [#17550](https://github.com/netbox-community/netbox/issues/17550) - Add IEEE 802.15.4 wireless interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16837](https://github.com/netbox-community/netbox/issues/16837) - Fix filtering of cables with no type assigned
|
||||
* [#17083](https://github.com/netbox-community/netbox/issues/17083) - Trim clickable area of form field labels
|
||||
* [#17126](https://github.com/netbox-community/netbox/issues/17126) - Show total device weight in both imperial & metric units
|
||||
* [#17360](https://github.com/netbox-community/netbox/issues/17360) - Fix AttributeError under child object views when experimental HTMX navigation is enabled
|
||||
* [#17406](https://github.com/netbox-community/netbox/issues/17406) - Fix the cleanup of stale custom field data after removing a plugin
|
||||
* [#17419](https://github.com/netbox-community/netbox/issues/17419) - Rebuild MPTT for module bays on upgrade to v4.1
|
||||
* [#17492](https://github.com/netbox-community/netbox/issues/17492) - Fix URL resolution in `NetBoxModelSerializer` for plugin models
|
||||
* [#17497](https://github.com/netbox-community/netbox/issues/17497) - Fix uncaught FieldError exception when referencing an invalid field on a related object during bulk import
|
||||
* [#17498](https://github.com/netbox-community/netbox/issues/17498) - Fix MultipleObjectsReturned exception when importing a device type without uniquely specifying a manufacturer
|
||||
* [#17501](https://github.com/netbox-community/netbox/issues/17501) - Fix reporting of last run time & status for custom scripts under UI
|
||||
* [#17511](https://github.com/netbox-community/netbox/issues/17511) - Restore consistent font support for non-Latin characters
|
||||
* [#17517](https://github.com/netbox-community/netbox/issues/17517) - Fix cable termination selection after switching termination type
|
||||
* [#17521](https://github.com/netbox-community/netbox/issues/17521) - Correct text color in notification pop-ups under dark mode
|
||||
* [#17522](https://github.com/netbox-community/netbox/issues/17522) - Fix language translation of form field labels under user preferences
|
||||
* [#17537](https://github.com/netbox-community/netbox/issues/17537) - Fix global search support for ASN range names
|
||||
* [#17555](https://github.com/netbox-community/netbox/issues/17555) - Fix toggling disconnected interfaces under device view
|
||||
* [#17601](https://github.com/netbox-community/netbox/issues/17601) - Record change to terminating object when disconnecting a cable
|
||||
* [#17605](https://github.com/netbox-community/netbox/issues/17605) - Fix calculation of aggregate VM disk space under cluster view
|
||||
* [#17611](https://github.com/netbox-community/netbox/issues/17611) - Correct custom field minimum value validation error message
|
||||
|
||||
---
|
||||
|
||||
## v4.1.1 (2024-09-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16926](https://github.com/netbox-community/netbox/issues/16926) - Add USB front & rear port types
|
||||
* [#17347](https://github.com/netbox-community/netbox/issues/17347) - Add NEMA L22-20 power port & outlet types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17066](https://github.com/netbox-community/netbox/issues/17066) - Fix OpenAPI schema definition for custom scripts REST API endpoint
|
||||
* [#17332](https://github.com/netbox-community/netbox/issues/17332) - Restore pagination for object list dashboard widgets
|
||||
* [#17333](https://github.com/netbox-community/netbox/issues/17333) - Avoid prefetching all jobs when retrieving custom scripts via the REST API
|
||||
* [#17353](https://github.com/netbox-community/netbox/issues/17353) - Fix styling of map buttons under site and device views
|
||||
* [#17354](https://github.com/netbox-community/netbox/issues/17354) - Prevent object & multi-object custom fields from breaking bulk import forms
|
||||
* [#17362](https://github.com/netbox-community/netbox/issues/17362) - Remove duplicate prefixes & IP addresses returned by the `present_in_vrf` query filter
|
||||
* [#17364](https://github.com/netbox-community/netbox/issues/17364) - Fix rendering of Markdown tables inside object list dashboard widgets
|
||||
* [#17387](https://github.com/netbox-community/netbox/issues/17387) - Fix display of the changelog tab for users with sufficient permission
|
||||
* [#17410](https://github.com/netbox-community/netbox/issues/17410) - Enable debug toolbar middleware for `strawberry-django` only when `DEBUG` is true
|
||||
* [#17414](https://github.com/netbox-community/netbox/issues/17414) - Fix support for declaring individual VLAN IDs within a VLAN group
|
||||
* [#17431](https://github.com/netbox-community/netbox/issues/17431) - Fix database migration error when upgrading to v4.1 from v3.7 or earlier
|
||||
* [#17437](https://github.com/netbox-community/netbox/issues/17437) - Fix exception when specifying a bridge relationship on an interface template
|
||||
* [#17444](https://github.com/netbox-community/netbox/issues/17444) - Custom script fails to execute when triggered by an event rule
|
||||
* [#17457](https://github.com/netbox-community/netbox/issues/17457) - GraphQL `service_list` filter should not require a port number
|
||||
|
||||
---
|
||||
|
||||
## v4.1.0 (2024-09-03)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
|
||||
* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly.
|
||||
* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending integer pairs.
|
||||
* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, indicating each assigned event type by name.
|
||||
* The UI views & API endpoints associate with change records have been moved from `/extras` to `/core`.
|
||||
* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by #14279).
|
||||
* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` has been changed from 1 gigabyte to 1 megabyte. Existing values will be adjusted automatically during the upgrade process.
|
||||
* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending VLAN ID pairs.
|
||||
* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, which lists applicable event types by name.
|
||||
* All UI views & API endpoints associated with change records have been moved from `/extras` to `/core`.
|
||||
* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by [#14279](https://github.com/netbox-community/netbox/issues/14279/)).
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -22,23 +81,23 @@ Circuits can now be assigned to groups for administrative purposes. Each circuit
|
||||
|
||||
#### VLAN Group ID Ranges ([#9627](https://github.com/netbox-community/netbox/issues/9627))
|
||||
|
||||
The VLAN group model has been enhanced to support multiple VLAN ID (VID) ranges, whereas previously it could track only a single beginning and ending VID. VID ranges are stored as an array of beginning and ending (inclusive) integer pairs.
|
||||
The VLAN group model has been enhanced to support multiple VLAN ID (VID) ranges, whereas previously it could track only a single beginning and ending VID pair. VID ranges are stored as an array of beginning and ending (inclusive) integer pairs, e.g. `1-100,1000-1999`.
|
||||
|
||||
#### Nested Device Modules ([#10500](https://github.com/netbox-community/netbox/issues/10500))
|
||||
|
||||
Module bays can now be nested to effect a hierarchical arrangement of modules within a device. A module installed within a device's module bay may itself have module bays into which child modules may be installed.
|
||||
Module bays can now be added to modules to effect a hierarchical arrangement of submodules within a device. A module installed within a device's module bay may itself have module bays into which child modules may be installed.
|
||||
|
||||
#### Rack Types ([#12826](https://github.com/netbox-community/netbox/issues/12826))
|
||||
|
||||
A new rack type model has been introduced, which functions similarly to the device type model. Users can now define a common make and model of rack, the attributes of which are automatically populated when creating a new rack of that type. Backward compatibility for racks with individually defined characteristics is fully retained.
|
||||
A new rack type model has been introduced, which functions similarly to device types. Users can now define a common make and model of equipment rack, the attributes of which are automatically populated when creating a new rack of that type. Backward compatibility for racks with individually defined characteristics is fully retained.
|
||||
|
||||
#### Plugins Catalog Integration ([#14731](https://github.com/netbox-community/netbox/issues/14731))
|
||||
|
||||
The NetBox UI now integrates directly with the canonical [plugins catalog](https://netboxlabs.com/netbox-plugins/) hosted by NetBox Labs. In addition to locally installed plugins, users can explore available plugins and check for newer releases.
|
||||
The NetBox UI now integrates directly with the canonical [plugins catalog](https://netboxlabs.com/netbox-plugins/) hosted by [NetBox Labs](https://netboxlabs.com). Users can now explore available plugins and check for newer releases natively within the NetBox user interface.
|
||||
|
||||
#### User Notifications ([#15621](https://github.com/netbox-community/netbox/issues/15621))
|
||||
|
||||
NetBox now includes a user notification system. Users can subscribe to individual objects and be alerted to changes live within the web interface. Additionally, event rules can now trigger notifications to specific users and/or groups. Plugins can also employ this notification system for their own purposes.
|
||||
NetBox now includes a user notification system. Users can subscribe to individual objects and be alerted to changes within the web interface. Additionally, event rules can be created to trigger notifications for specific users and/or groups. Plugins can also employ this notification system for their own purposes.
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -46,41 +105,65 @@ NetBox now includes a user notification system. Users can subscribe to individua
|
||||
* [#8198](https://github.com/netbox-community/netbox/issues/8198) - Enable uniqueness enforcement for custom field values
|
||||
* [#8984](https://github.com/netbox-community/netbox/issues/8984) - Enable filtering of custom script output by log level
|
||||
* [#11969](https://github.com/netbox-community/netbox/issues/11969) - Support for tracking airflow on racks and module types
|
||||
* [#14656](https://github.com/netbox-community/netbox/issues/14656) - Dynamically render custom field edit form depending on the selected field type
|
||||
* [#15106](https://github.com/netbox-community/netbox/issues/15106) - Add distance tracking for wireless links
|
||||
* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers
|
||||
* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable individual views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
|
||||
* [#16782](https://github.com/netbox-community/netbox/issues/16782) - Enable filtering of selection choices for object type custom fields
|
||||
* [#16907](https://github.com/netbox-community/netbox/issues/16907) - Updated user interface styling
|
||||
* [#17051](https://github.com/netbox-community/netbox/issues/17051) - Introduced `ISOLATED_DEPLOYMENT` config parameter
|
||||
* [#14656](https://github.com/netbox-community/netbox/issues/14656) - Dynamically render the custom field edit form depending on the selected field type
|
||||
* [#15106](https://github.com/netbox-community/netbox/issues/15106) - Add `distance` and `distance_unit` fields for wireless links
|
||||
* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers, which links to the corresponding UI view for an object
|
||||
* [#16574](https://github.com/netbox-community/netbox/issues/16574) - Add `last_synced` time to REST API serializer for data sources
|
||||
* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable plugin views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
|
||||
* [#16782](https://github.com/netbox-community/netbox/issues/16782) - Enable filtering of selection choices for object and multi-object custom fields
|
||||
* [#16907](https://github.com/netbox-community/netbox/issues/16907) - Update user interface styling
|
||||
* [#17051](https://github.com/netbox-community/netbox/issues/17051) - Introduce `ISOLATED_DEPLOYMENT` config parameter for denoting Internet isolation
|
||||
* [#17221](https://github.com/netbox-community/netbox/issues/17221) - `ObjectEditView` now supports HTMX-based object editing
|
||||
* [#17288](https://github.com/netbox-community/netbox/issues/17288) - Introduce a configurable limit on the number of aliases within a GraphQL API request
|
||||
* [#17289](https://github.com/netbox-community/netbox/issues/17289) - Enforce a standard policy for local passwords by default
|
||||
* [#17318](https://github.com/netbox-community/netbox/issues/17318) - Include the assigned provider in nested API representation of circuits
|
||||
|
||||
### Bug Fixes (From Beta1)
|
||||
|
||||
* [#17086](https://github.com/netbox-community/netbox/issues/17086) - Fix exception when viewing a job with no related object
|
||||
* [#17097](https://github.com/netbox-community/netbox/issues/17097) - Record static object representation when calling `NotificationGroup.notify()`
|
||||
* [#17098](https://github.com/netbox-community/netbox/issues/17098) - Prevent automatic deletion of related notifications when deleting an object
|
||||
* [#17159](https://github.com/netbox-community/netbox/issues/17159) - Correct file paths in plugin installation instructions
|
||||
* [#17163](https://github.com/netbox-community/netbox/issues/17163) - Fix filtering of related services under IP address view
|
||||
* [#17169](https://github.com/netbox-community/netbox/issues/17169) - Avoid duplicating catalog listings for installed plugins
|
||||
* [#17301](https://github.com/netbox-community/netbox/issues/17301) - Correct styling of the edit & delete buttons for custom script modules
|
||||
* [#17302](https://github.com/netbox-community/netbox/issues/17302) - Fix log level filtering support for custom script messages
|
||||
* [#17306](https://github.com/netbox-community/netbox/issues/17306) - Correct rounding of reported VLAN group utilization
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#15692](https://github.com/netbox-community/netbox/issues/15692) - Introduce improved plugin support for background jobs
|
||||
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
|
||||
* [#16726](https://github.com/netbox-community/netbox/issues/16726) - Extend `PluginTemplateExtension` to enable registering multiple models
|
||||
* [#16776](https://github.com/netbox-community/netbox/issues/16776) - Added an `alerts()` method to `PluginTemplateExtension` for embedding important information about specific objects
|
||||
* [#16886](https://github.com/netbox-community/netbox/issues/16886) - Introduced a mechanism for plugins to register custom event types (for use with user notifications)
|
||||
* [#16776](https://github.com/netbox-community/netbox/issues/16776) - Add an `alerts()` method to `PluginTemplateExtension` for embedding important information on object views
|
||||
* [#16886](https://github.com/netbox-community/netbox/issues/16886) - Introduce a mechanism for plugins to register custom event types (for use with user notifications)
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change atomic unit for virtual disks from 1GB to 1MB
|
||||
* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change the atomic unit for virtual disks from 1GB to 1MB
|
||||
* [#14861](https://github.com/netbox-community/netbox/issues/14861) - The URL path for UI views concerning virtual disks has been standardized to `/virtualization/virtual-disks/`
|
||||
* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Removed various deprecated filters
|
||||
* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Remove various deprecated query filters
|
||||
* [#15908](https://github.com/netbox-community/netbox/issues/15908) - Indicate product edition in release data
|
||||
* [#16388](https://github.com/netbox-community/netbox/issues/16388) - Move all change logging resources from `extras` to `core`
|
||||
* [#16884](https://github.com/netbox-community/netbox/issues/16884) - Remove the ID column from the default table configuration for changelog records
|
||||
* [#16988](https://github.com/netbox-community/netbox/issues/16988) - Relocated rack items in navigation menu
|
||||
* [#16988](https://github.com/netbox-community/netbox/issues/16988) - Relocate rack items in navigation menu
|
||||
* [#17143](https://github.com/netbox-community/netbox/issues/17143) - The use of legacy "nested" serializer classes has been deprecated
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`
|
||||
* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`.
|
||||
* Most object representations now include a read-only `display_url` field, which links to the object's corresponding UI view.
|
||||
* Added the following endpoints:
|
||||
* `/api/circuits/circuit-groups/`
|
||||
* `/api/circuits/circuit-group-assignments/`
|
||||
* `/api/dcim/rack-types/`
|
||||
* `/api/extras/notification-groups/`
|
||||
* `/api/extras/notifications/`
|
||||
* `/api/extras/subscriptions/`
|
||||
* circuits.Circuit
|
||||
* Added the `assignments` field, which lists all group assignments
|
||||
* core.DataSource
|
||||
* Added the read-only `last_synced` field
|
||||
* dcim.ModuleBay
|
||||
* Added the optional `module` foreign key field
|
||||
* dcim.ModuleBayTemplate
|
||||
@@ -92,12 +175,13 @@ NetBox now includes a user notification system. Users can subscribe to individua
|
||||
* Added the optional `airflow` choice field
|
||||
* extras.CustomField
|
||||
* Added the `related_object_filter` JSON field for object and multi-object custom fields
|
||||
* Added the `validation_unique` boolean field
|
||||
* extras.EventRule
|
||||
* Removed the `type_create`, `type_update`, `type_delete`, `type_job_start`, and `type_job_end` boolean fields
|
||||
* Added the `event_types` array field
|
||||
* ipam.VLANGroup
|
||||
* Removed the `min_vid` and `max_vid` fields
|
||||
* Added the `vid_ranges` field, and array of starting & ending VLAN IDs
|
||||
* Added the `vid_ranges` field, an array of starting & ending VLAN IDs
|
||||
* virtualization.VirtualMachine
|
||||
* Added the optional `serial` field
|
||||
* wireless.WirelessLink
|
||||
|
||||
@@ -109,6 +109,7 @@ nav:
|
||||
- Required Parameters: 'configuration/required-parameters.md'
|
||||
- System: 'configuration/system.md'
|
||||
- Security: 'configuration/security.md'
|
||||
- GraphQL API: 'configuration/graphql-api.md'
|
||||
- Remote Authentication: 'configuration/remote-authentication.md'
|
||||
- Data & Validation: 'configuration/data-validation.md'
|
||||
- Default Values: 'configuration/default-values.md'
|
||||
|
||||
@@ -111,7 +111,7 @@ class LoginView(View):
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
logger.info(f"User {request.user} successfully authenticated")
|
||||
messages.success(request, f"Logged in as {request.user}.")
|
||||
messages.success(request, _("Logged in as {user}.").format(user=request.user))
|
||||
|
||||
# Ensure the user has a UserConfig defined. (This should normally be handled by
|
||||
# create_userconfig() on user creation.)
|
||||
@@ -161,7 +161,7 @@ class LogoutView(View):
|
||||
username = request.user
|
||||
auth_logout(request)
|
||||
logger.info(f"User {username} has logged out")
|
||||
messages.info(request, "You have logged out.")
|
||||
messages.info(request, _("You have logged out."))
|
||||
|
||||
# Delete session key & language cookies (if set) upon logout
|
||||
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
|
||||
@@ -236,7 +236,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
# LDAP users cannot change their password here
|
||||
if getattr(request.user, 'ldap_username', None):
|
||||
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
||||
messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox."))
|
||||
return redirect('account:profile')
|
||||
|
||||
form = PasswordChangeForm(user=request.user)
|
||||
@@ -251,7 +251,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, "Your password has been changed successfully.")
|
||||
messages.success(request, _("Your password has been changed successfully."))
|
||||
return redirect('account:profile')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import warnings
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .serializers_.nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
@@ -14,6 +16,12 @@ __all__ = [
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
@@ -41,17 +49,6 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
from .serializers_.providers import *
|
||||
from .serializers_.circuits import *
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
@@ -90,7 +88,7 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
|
||||
13
netbox/circuits/api/serializers_/nested.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from circuits.models import ProviderAccount
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedProviderAccountSerializer',
|
||||
)
|
||||
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
|
||||
@@ -5,7 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
|
||||
from ipam.models import ASN
|
||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from ..nested_serializers import *
|
||||
from .nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = (
|
||||
'ProviderAccountSerializer',
|
||||
|
||||
@@ -319,9 +319,26 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
label=_('Circuit (ID)'),
|
||||
)
|
||||
circuit = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__cid',
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
@@ -336,7 +353,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('id', 'circuit', 'group', 'priority')
|
||||
fields = ('id', 'priority')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -247,7 +247,12 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
|
||||
@@ -3,48 +3,31 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit: CircuitType = strawberry_django.field()
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group(self, id: int) -> CircuitGroupType:
|
||||
return models.CircuitGroup.objects.get(pk=id)
|
||||
circuit_group: CircuitGroupType = strawberry_django.field()
|
||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group_assignment(self, id: int) -> CircuitGroupAssignmentType:
|
||||
return models.CircuitGroupAssignment.objects.get(pk=id)
|
||||
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
provider: ProviderType = strawberry_django.field()
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account: ProviderAccountType = strawberry_django.field()
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-22 06:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
@@ -10,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0118_notifications'),
|
||||
('extras', '0119_notifications'),
|
||||
('tenancy', '0015_contactassignment_rename_content_type'),
|
||||
]
|
||||
|
||||
|
||||
@@ -155,6 +155,11 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
verbose_name=_('Group'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor='circuit__provider',
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
circuit = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
@@ -169,6 +174,6 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroupAssignment
|
||||
fields = (
|
||||
'pk', 'id', 'group', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
||||
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'group', 'circuit', 'priority')
|
||||
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
|
||||
|
||||
@@ -92,10 +92,11 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'url']
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
user_permissions = ('circuits.view_provider', 'circuits.view_circuittype')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -150,6 +151,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitTermination
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
|
||||
user_permissions = ('circuits.view_circuit', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -241,6 +243,7 @@ class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||
user_permissions = ('circuits.view_provider',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -287,6 +290,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -355,6 +359,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
user_permissions = ('circuits.view_provider', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -524,14 +524,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
providers = Provider.objects.bulk_create((
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
))
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@@ -561,10 +566,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_circuit_id(self):
|
||||
circuits = Circuit.objects.filter(cid__in=['Circuit 1', 'Circuit 2'])
|
||||
def test_circuit(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from netbox.views import generic
|
||||
@@ -326,7 +327,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
# Circuit must have at least one termination to swap
|
||||
if not circuit.termination_a and not circuit.termination_z:
|
||||
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||
messages.error(request, _(
|
||||
"No terminations have been defined for circuit {circuit}."
|
||||
).format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
@@ -374,7 +377,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
circuit.termination_z = None
|
||||
circuit.save()
|
||||
|
||||
messages.success(request, f"Swapped terminations for circuit {circuit}.")
|
||||
messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
@@ -12,6 +14,12 @@ __all__ = (
|
||||
'NestedJobSerializer',
|
||||
)
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
@@ -126,9 +126,18 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
|
||||
return response_serializers
|
||||
|
||||
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
|
||||
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
|
||||
|
||||
# If this serializer is nested, prepend its name with "Brief"
|
||||
if getattr(serializer, 'nested', False):
|
||||
name = f'Brief{name}'
|
||||
|
||||
return name
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
"""Get serializer's ref_name
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
@@ -137,8 +146,6 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.data import *
|
||||
from .serializers_.jobs import *
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import DataFile, DataSource
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
@@ -27,8 +25,9 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
|
||||
'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', 'last_synced',
|
||||
'file_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
@@ -3,18 +3,13 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CoreQuery:
|
||||
@strawberry.field
|
||||
def data_file(self, id: int) -> DataFileType:
|
||||
return models.DataFile.objects.get(pk=id)
|
||||
data_file: DataFileType = strawberry_django.field()
|
||||
data_file_list: List[DataFileType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def data_source(self, id: int) -> DataSourceType:
|
||||
return models.DataSource.objects.get(pk=id)
|
||||
data_source: DataSourceType = strawberry_django.field()
|
||||
data_source_list: List[DataSourceType] = strawberry_django.field()
|
||||
|
||||
@@ -11,6 +11,10 @@ from core.models import ObjectType
|
||||
from users.models import User
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||
EXCLUDE_MODELS = (
|
||||
'extras.branch',
|
||||
'extras.stagedchange',
|
||||
)
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
@@ -44,12 +48,16 @@ class Command(BaseCommand):
|
||||
|
||||
# Gather Django models and constants from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
models = []
|
||||
|
||||
# Load models from each app
|
||||
for model in apps.get_app_config(app).get_models():
|
||||
namespace[model.__name__] = model
|
||||
self.django_models[app].append(model.__name__)
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
if f'{app_label}.{model_name}' not in EXCLUDE_MODELS:
|
||||
namespace[model.__name__] = model
|
||||
models.append(model.__name__)
|
||||
self.django_models[app] = sorted(models)
|
||||
|
||||
# Constants
|
||||
try:
|
||||
|
||||
@@ -81,8 +81,9 @@ def get_local_plugins(plugins=None):
|
||||
plugin_config: PluginConfig = plugin.config
|
||||
|
||||
local_plugins[plugin_config.name] = Plugin(
|
||||
slug=plugin_config.name,
|
||||
config_name=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
title_long=plugin_config.verbose_name,
|
||||
tag_line=plugin_config.description,
|
||||
description_short=plugin_config.description,
|
||||
is_local=True,
|
||||
@@ -95,6 +96,7 @@ def get_local_plugins(plugins=None):
|
||||
if k in plugins:
|
||||
plugins[k].is_local = True
|
||||
plugins[k].is_installed = True
|
||||
plugins[k].installed_version = v.installed_version
|
||||
else:
|
||||
plugins[k] = v
|
||||
|
||||
@@ -181,7 +183,7 @@ def get_catalog_plugins():
|
||||
author = None
|
||||
|
||||
# Populate plugin data
|
||||
plugins[data['slug']] = Plugin(
|
||||
plugins[data['config_name']] = Plugin(
|
||||
id=data['id'],
|
||||
status=data['status'],
|
||||
title_short=data['title_short'],
|
||||
|
||||
@@ -39,8 +39,8 @@ class PluginVersionTable(BaseTable):
|
||||
|
||||
|
||||
class CatalogPluginTable(BaseTable):
|
||||
title_short = tables.Column(
|
||||
linkify=('core:plugin', [tables.A('slug')]),
|
||||
title_long = tables.Column(
|
||||
linkify=('core:plugin', [tables.A('config_name')]),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
author = tables.Column(
|
||||
@@ -63,17 +63,21 @@ class CatalogPluginTable(BaseTable):
|
||||
verbose_name=_('Updated')
|
||||
)
|
||||
installed_version = tables.Column(
|
||||
verbose_name=_('Installed version')
|
||||
verbose_name=_('Installed Version')
|
||||
)
|
||||
latest_version = tables.Column(
|
||||
accessor=tables.A('release_latest__version'),
|
||||
verbose_name=_('Latest Version')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No plugin data found')
|
||||
fields = (
|
||||
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
'installed_version',
|
||||
'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
'installed_version', 'latest_version',
|
||||
)
|
||||
default_columns = (
|
||||
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'installed_version', 'latest_version',
|
||||
)
|
||||
# List installed plugins first, then certified plugins, then
|
||||
# everything else (with each tranche ordered alphabetically)
|
||||
|
||||
@@ -57,6 +57,7 @@ class DataFileTest(
|
||||
):
|
||||
model = DataFile
|
||||
brief_fields = ['display', 'id', 'path', 'url']
|
||||
user_permissions = ('core.view_datasource', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import platform
|
||||
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
@@ -32,6 +31,7 @@ from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.json import ConfigJSONEncoder
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@@ -85,7 +85,10 @@ class DataSourceSyncView(BaseObjectView):
|
||||
datasource.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
|
||||
|
||||
messages.success(request, f"Queued job #{job.pk} to sync {datasource}")
|
||||
messages.success(
|
||||
request,
|
||||
_("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
|
||||
)
|
||||
return redirect(datasource.get_absolute_url())
|
||||
|
||||
|
||||
@@ -313,7 +316,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
|
||||
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
|
||||
candidate_config.activate()
|
||||
messages.success(request, f"Restored configuration revision #{pk}")
|
||||
messages.success(request, _("Restored configuration revision #{id}").format(id=pk))
|
||||
|
||||
return redirect(candidate_config.get_absolute_url())
|
||||
|
||||
@@ -457,9 +460,9 @@ class BackgroundTaskDeleteView(BaseRQView):
|
||||
# Remove job id from queue and delete the actual job
|
||||
queue.connection.lrem(queue.key, 0, job.id)
|
||||
job.delete()
|
||||
messages.success(request, f'Deleted job {job_id}')
|
||||
messages.success(request, _('Job {id} has been deleted.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Error deleting job: {form.errors[0]}')
|
||||
messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0]))
|
||||
|
||||
return redirect(reverse('core:background_queue_list'))
|
||||
|
||||
@@ -472,13 +475,13 @@ class BackgroundTaskRequeueView(BaseRQView):
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
raise Http404(_("Job {id} not found.").format(id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
requeue_job(job_id, connection=queue.connection, serializer=queue.serializer)
|
||||
messages.success(request, f'You have successfully requeued: {job_id}')
|
||||
messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
@@ -490,7 +493,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
raise Http404(_("Job {id} not found.").format(id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
@@ -513,7 +516,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
|
||||
registry = ScheduledJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
|
||||
messages.success(request, f'You have successfully enqueued: {job_id}')
|
||||
messages.success(request, _('Job {id} has been enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
@@ -530,11 +533,11 @@ class BackgroundTaskStopView(BaseRQView):
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
stopped, _ = stop_jobs(queue, job_id)
|
||||
if len(stopped) == 1:
|
||||
messages.success(request, f'You have successfully stopped {job_id}')
|
||||
stopped_jobs = stop_jobs(queue, job_id)[0]
|
||||
if len(stopped_jobs) == 1:
|
||||
messages.success(request, _('Job {id} has been stopped.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Failed to stop {job_id}')
|
||||
messages.error(request, _('Failed to stop job {id}').format(id=job_id))
|
||||
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
@@ -640,10 +643,14 @@ class SystemView(UserPassesTestMixin, View):
|
||||
k: getattr(config, k) for k in sorted(params)
|
||||
},
|
||||
}
|
||||
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
||||
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
|
||||
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
'config': config,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import warnings
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .serializers_.nested import (
|
||||
NestedDeviceBaySerializer, NestedDeviceSerializer, NestedInterfaceSerializer, NestedInterfaceTemplateSerializer,
|
||||
NestedLocationSerializer, NestedModuleBaySerializer, NestedRegionSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
@@ -48,35 +54,17 @@ __all__ = [
|
||||
'NestedVirtualDeviceContextSerializer',
|
||||
]
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -88,18 +76,6 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
@@ -200,13 +176,6 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -271,13 +240,6 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -285,13 +247,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display_url', 'display', 'serial']
|
||||
|
||||
|
||||
class NestedModuleSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
|
||||
@@ -338,15 +293,6 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
@@ -365,22 +311,6 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display_url', 'display', 'installed_module', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
@@ -11,4 +11,3 @@ from .serializers_.devices import *
|
||||
from .serializers_.device_components import *
|
||||
from .serializers_.power import *
|
||||
from .serializers_.rackunits import *
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -15,7 +15,7 @@ from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelated
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||
from wireless.api.nested_serializers import NestedWirelessLinkSerializer
|
||||
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
||||
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||
from wireless.choices import *
|
||||
from wireless.models import WirelessLAN
|
||||
@@ -23,8 +23,8 @@ from .base import ConnectedEndpointsSerializer
|
||||
from .cables import CabledObjectSerializer
|
||||
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .nested import NestedInterfaceSerializer
|
||||
from .roles import InventoryItemRoleSerializer
|
||||
from ..nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortSerializer',
|
||||
@@ -299,7 +299,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
fields=('id', 'url', 'display', 'module_bay'),
|
||||
fields=('id', 'url', 'display'),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
|
||||
@@ -16,9 +16,9 @@ from .devicetypes import *
|
||||
from .platforms import PlatformSerializer
|
||||
from .racks import RackSerializer
|
||||
from .roles import DeviceRoleSerializer
|
||||
from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
|
||||
from .sites import LocationSerializer, SiteSerializer
|
||||
from .virtualchassis import VirtualChassisSerializer
|
||||
from ..nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'DeviceSerializer',
|
||||
|
||||
@@ -14,8 +14,8 @@ from utilities.api import get_serializer_for_model
|
||||
from wireless.choices import *
|
||||
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .nested import NestedInterfaceTemplateSerializer
|
||||
from .roles import InventoryItemRoleSerializer
|
||||
from ..nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortTemplateSerializer',
|
||||
|
||||
97
netbox/dcim/api/serializers_/nested.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from dcim import models
|
||||
|
||||
__all__ = (
|
||||
'NestedDeviceBaySerializer',
|
||||
'NestedDeviceSerializer',
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedInterfaceTemplateSerializer',
|
||||
'NestedLocationSerializer',
|
||||
'NestedModuleBaySerializer',
|
||||
'NestedRegionSerializer',
|
||||
'NestedSiteGroupSerializer',
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display_url', 'display', 'serial']
|
||||
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
@@ -64,18 +64,13 @@ class RackTypeSerializer(RackBaseSerializer):
|
||||
manufacturer = ManufacturerSerializer(
|
||||
nested=True
|
||||
)
|
||||
airflow = ChoiceField(
|
||||
choices=RackAirflowChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
|
||||
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'mounting_depth', 'airflow', 'description', 'comments', 'tags',
|
||||
'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
|
||||
|
||||
@@ -8,7 +8,7 @@ from ipam.models import ASN
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from ..nested_serializers import *
|
||||
from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer
|
||||
|
||||
__all__ = (
|
||||
'LocationSerializer',
|
||||
|
||||
@@ -2,7 +2,7 @@ from rest_framework import serializers
|
||||
|
||||
from dcim.models import VirtualChassis
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from ..nested_serializers import *
|
||||
from .nested import NestedDeviceSerializer
|
||||
|
||||
__all__ = (
|
||||
'VirtualChassisSerializer',
|
||||
|
||||
@@ -129,12 +129,12 @@ class RackElevationDetailRenderChoices(ChoiceSet):
|
||||
|
||||
class RackAirflowChoices(ChoiceSet):
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
FRONT_TO_REAR = 'front-to-rear'
|
||||
REAR_TO_FRONT = 'rear-to-front'
|
||||
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
|
||||
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
|
||||
(FRONT_TO_REAR, _('Front to rear')),
|
||||
(REAR_TO_FRONT, _('Rear to front')),
|
||||
)
|
||||
|
||||
|
||||
@@ -237,20 +237,20 @@ class ModuleStatusChoices(ChoiceSet):
|
||||
|
||||
class ModuleAirflowChoices(ChoiceSet):
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
|
||||
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
|
||||
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
FRONT_TO_REAR = 'front-to-rear'
|
||||
REAR_TO_FRONT = 'rear-to-front'
|
||||
LEFT_TO_RIGHT = 'left-to-right'
|
||||
RIGHT_TO_LEFT = 'right-to-left'
|
||||
SIDE_TO_REAR = 'side-to-rear'
|
||||
PASSIVE = 'passive'
|
||||
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
|
||||
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
|
||||
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
|
||||
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
|
||||
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
|
||||
(AIRFLOW_PASSIVE, _('Passive')),
|
||||
(FRONT_TO_REAR, _('Front to rear')),
|
||||
(REAR_TO_FRONT, _('Rear to front')),
|
||||
(LEFT_TO_RIGHT, _('Left to right')),
|
||||
(RIGHT_TO_LEFT, _('Right to left')),
|
||||
(SIDE_TO_REAR, _('Side to rear')),
|
||||
(PASSIVE, _('Passive')),
|
||||
)
|
||||
|
||||
|
||||
@@ -396,6 +396,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560P = 'nema-l15-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
TYPE_NEMA_L2220P = 'nema-l22-20p'
|
||||
TYPE_NEMA_L2230P = 'nema-l22-30p'
|
||||
# California style
|
||||
TYPE_CS6361C = 'cs6361c'
|
||||
@@ -517,6 +518,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
(TYPE_NEMA_L2220P, 'NEMA L22-20P'),
|
||||
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
|
||||
)),
|
||||
(_('California Style'), (
|
||||
@@ -649,6 +651,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560R = 'nema-l15-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
TYPE_NEMA_L2220R = 'nema-l22-20r'
|
||||
TYPE_NEMA_L2230R = 'nema-l22-30r'
|
||||
# California style
|
||||
TYPE_CS6360C = 'CS6360C'
|
||||
@@ -681,6 +684,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
# Direct current (DC)
|
||||
TYPE_DC = 'dc-terminal'
|
||||
# Proprietary
|
||||
TYPE_EATON_C39 = 'eaton-c39'
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
|
||||
@@ -763,6 +767,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
(TYPE_NEMA_L2220R, 'NEMA L22-20R'),
|
||||
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
|
||||
)),
|
||||
(_('California Style'), (
|
||||
@@ -801,6 +806,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_DC, 'DC Terminal'),
|
||||
)),
|
||||
(_('Proprietary'), (
|
||||
(TYPE_EATON_C39, 'Eaton C39'),
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
@@ -857,6 +863,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_100ME_LFX = '100base-lfx'
|
||||
TYPE_100ME_FIXED = '100base-tx'
|
||||
TYPE_100ME_T1 = '100base-t1'
|
||||
TYPE_100ME_SFP = '100base-x-sfp'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_TX_FIXED = '1000base-tx'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
@@ -916,7 +923,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_80211AY = 'ieee802.11ay'
|
||||
TYPE_80211BE = 'ieee802.11be'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
TYPE_802154 = 'ieee802.15.4'
|
||||
TYPE_OTHER_WIRELESS = 'other-wireless'
|
||||
|
||||
# Cellular
|
||||
@@ -1028,6 +1037,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(
|
||||
_('Ethernet (modular)'),
|
||||
(
|
||||
(TYPE_100ME_SFP, 'SFP (100ME)'),
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
@@ -1087,7 +1097,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_80211AY, 'IEEE 802.11ay'),
|
||||
(TYPE_80211BE, 'IEEE 802.11be'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
(TYPE_802154, 'IEEE 802.15.4 (LR-WPAN)'),
|
||||
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
|
||||
)
|
||||
),
|
||||
@@ -1345,6 +1357,14 @@ class PortTypeChoices(ChoiceSet):
|
||||
TYPE_URM_P2 = 'urm-p2'
|
||||
TYPE_URM_P4 = 'urm-p4'
|
||||
TYPE_URM_P8 = 'urm-p8'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
TYPE_USB_MINI_A = 'usb-mini-a'
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
@@ -1404,6 +1424,19 @@ class PortTypeChoices(ChoiceSet):
|
||||
(TYPE_SPLICE, 'Splice'),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('USB'),
|
||||
(
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
(TYPE_USB_B, 'USB Type B'),
|
||||
(TYPE_USB_C, 'USB Type C'),
|
||||
(TYPE_USB_MINI_A, 'USB Mini A'),
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('Other'),
|
||||
(
|
||||
@@ -1442,6 +1475,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
TYPE_SMF_OS2 = 'smf-os2'
|
||||
TYPE_AOC = 'aoc'
|
||||
TYPE_POWER = 'power'
|
||||
TYPE_USB = 'usb'
|
||||
|
||||
CHOICES = (
|
||||
(
|
||||
@@ -1474,6 +1508,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
(TYPE_AOC, 'Active Optical Cabling (AOC)'),
|
||||
),
|
||||
),
|
||||
(TYPE_USB, _('USB')),
|
||||
(TYPE_POWER, _('Power')),
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
InterfaceTypeChoices.TYPE_80211AX,
|
||||
InterfaceTypeChoices.TYPE_80211AY,
|
||||
InterfaceTypeChoices.TYPE_80211BE,
|
||||
InterfaceTypeChoices.TYPE_802151,
|
||||
InterfaceTypeChoices.TYPE_802154,
|
||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||
]
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
|
||||
model = RackType
|
||||
fields = (
|
||||
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -1463,6 +1463,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label=_('Virtual Chassis'),
|
||||
)
|
||||
device_status = django_filters.MultipleChoiceFilter(
|
||||
choices=DeviceStatusChoices,
|
||||
field_name='device__status',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -268,11 +268,6 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(RackAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
min_value=0,
|
||||
@@ -298,7 +293,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
||||
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
@@ -1295,12 +1290,17 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, *args, initial=None, **kwargs):
|
||||
try:
|
||||
self.device_id = int(initial.get('device'))
|
||||
except (TypeError, ValueError):
|
||||
self.device_id = None
|
||||
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
|
||||
# Limit module queryset to Modules which belong to the parent Device
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
self.fields['module'].queryset = Module.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['module'].choices = ()
|
||||
@@ -1308,8 +1308,8 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1325,8 +1325,8 @@ class ConsolePortBulkEditForm(
|
||||
|
||||
|
||||
class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1342,8 +1342,8 @@ class ConsoleServerPortBulkEditForm(
|
||||
|
||||
|
||||
class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1360,8 +1360,8 @@ class PowerPortBulkEditForm(
|
||||
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1380,8 +1380,8 @@ class PowerOutletBulkEditForm(
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['power_port'].choices = ()
|
||||
@@ -1389,12 +1389,12 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power', 'wireless_lans'
|
||||
]),
|
||||
ComponentBulkEditForm
|
||||
])
|
||||
):
|
||||
enabled = forms.NullBooleanField(
|
||||
label=_('Enabled'),
|
||||
@@ -1523,8 +1523,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
if self.device_id:
|
||||
device = Device.objects.filter(pk=self.device_id).first()
|
||||
|
||||
# Restrict parent/bridge/LAG interface assignment by device
|
||||
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
|
||||
@@ -1587,8 +1587,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
@@ -1604,8 +1604,8 @@ class FrontPortBulkEditForm(
|
||||
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
ComponentBulkEditForm
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description'])
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
|
||||
@@ -9,7 +9,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import VRF
|
||||
from ipam.models import VRF, IPAddress
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import (
|
||||
@@ -206,12 +206,6 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Unit for outer dimensions')
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=RackAirflowChoices,
|
||||
required=False,
|
||||
help_text=_('Airflow direction')
|
||||
)
|
||||
weight_unit = CSVChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=WeightUnitChoices,
|
||||
@@ -223,7 +217,7 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
||||
model = RackType
|
||||
fields = (
|
||||
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
@@ -373,13 +367,13 @@ class ManufacturerImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class DeviceTypeImportForm(NetBoxModelImportForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
manufacturer = CSVModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('The manufacturer which produces this device type')
|
||||
)
|
||||
default_platform = forms.ModelChoiceField(
|
||||
default_platform = CSVModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='name',
|
||||
@@ -1441,9 +1435,33 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
|
||||
label=_('Status'),
|
||||
choices=VirtualDeviceContextStatusChoices,
|
||||
)
|
||||
primary_ip4 = CSVModelChoiceField(
|
||||
label=_('Primary IPv4'),
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
to_field_name='address',
|
||||
help_text=_('IPv4 address with mask, e.g. 1.2.3.4/24')
|
||||
)
|
||||
primary_ip6 = CSVModelChoiceField(
|
||||
label=_('Primary IPv6'),
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
to_field_name='address',
|
||||
help_text=_('IPv6 address with prefix length, e.g. 2001:db8::1/64')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'name', 'device', 'status', 'tenant', 'identifier', 'comments',
|
||||
'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
|
||||
]
|
||||
model = VirtualDeviceContext
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit primary_ip4/ip6 querysets by assigned device
|
||||
params = {f"interface__device__{self.fields['device'].to_field_name}": data.get('device')}
|
||||
self.fields['primary_ip4'].queryset = self.fields['primary_ip4'].queryset.filter(**params)
|
||||
self.fields['primary_ip6'].queryset = self.fields['primary_ip6'].queryset.filter(**params)
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_cable_form(a_type, b_type):
|
||||
# Device component
|
||||
if hasattr(term_cls, 'device'):
|
||||
|
||||
attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label=_('Device'),
|
||||
required=False,
|
||||
@@ -33,6 +33,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=term_cls._meta.verbose_name.title(),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'device',
|
||||
},
|
||||
query_params={
|
||||
'device_id': f'$termination_{cable_end}_device',
|
||||
@@ -43,7 +44,7 @@ def get_cable_form(a_type, b_type):
|
||||
# PowerFeed
|
||||
elif term_cls == PowerFeed:
|
||||
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label=_('Power Panel'),
|
||||
required=False,
|
||||
@@ -57,6 +58,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=_('Power Feed'),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'powerpanel',
|
||||
},
|
||||
query_params={
|
||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||
@@ -66,7 +68,7 @@ def get_cable_form(a_type, b_type):
|
||||
# CircuitTermination
|
||||
elif term_cls == CircuitTermination:
|
||||
|
||||
attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
|
||||
attrs[f'termination_{cable_end}_circuit'] = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
selector=True,
|
||||
@@ -79,6 +81,7 @@ def get_cable_form(a_type, b_type):
|
||||
label=_('Side'),
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
'parent': 'circuit',
|
||||
},
|
||||
query_params={
|
||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||
|
||||
@@ -130,6 +130,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
device_status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
label=_('Device Status'),
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
@@ -196,7 +201,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
@@ -233,6 +238,10 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
choices=LocationStatusChoices,
|
||||
required=False
|
||||
)
|
||||
facility = forms.CharField(
|
||||
label=_('Facility'),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -293,7 +302,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
@@ -1229,7 +1238,9 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1251,7 +1262,10 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1273,7 +1287,9 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1290,7 +1306,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1310,7 +1329,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
@@ -1418,7 +1440,9 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
model = FrontPort
|
||||
@@ -1440,7 +1464,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1461,7 +1488,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
position = forms.CharField(
|
||||
@@ -1476,7 +1506,10 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1490,7 +1523,10 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
|
||||
@@ -214,7 +214,7 @@ class RackTypeForm(NetBoxModelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')),
|
||||
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'width', 'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
@@ -229,7 +229,7 @@ class RackTypeForm(NetBoxModelForm):
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'airflow', 'description', 'comments', 'tags',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'tags', name=_('Rack')),
|
||||
FieldSet('site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags', name=_('Rack')),
|
||||
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@@ -293,7 +293,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
self.fieldsets = (
|
||||
*self.fieldsets,
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'starting_unit', 'u_height', 'airflow',
|
||||
'form_factor', 'width', 'starting_unit', 'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', 'desc_units', name=_('Dimensions')
|
||||
@@ -975,8 +975,8 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
'device_type_id': '$device_type',
|
||||
'module_type_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1351,7 +1351,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN group')
|
||||
label=_('VLAN group'),
|
||||
help_text=_("Filter VLANs available for assignment by group.")
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
|
||||
@@ -3,213 +3,130 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from dcim import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class DCIMQuery:
|
||||
@strawberry.field
|
||||
def cable(self, id: int) -> CableType:
|
||||
return models.Cable.objects.get(pk=id)
|
||||
cable: CableType = strawberry_django.field()
|
||||
cable_list: List[CableType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port(self, id: int) -> ConsolePortType:
|
||||
return models.ConsolePort.objects.get(pk=id)
|
||||
console_port: ConsolePortType = strawberry_django.field()
|
||||
console_port_list: List[ConsolePortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port_template(self, id: int) -> ConsolePortTemplateType:
|
||||
return models.ConsolePortTemplate.objects.get(pk=id)
|
||||
console_port_template: ConsolePortTemplateType = strawberry_django.field()
|
||||
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port(self, id: int) -> ConsoleServerPortType:
|
||||
return models.ConsoleServerPort.objects.get(pk=id)
|
||||
console_server_port: ConsoleServerPortType = strawberry_django.field()
|
||||
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
|
||||
return models.ConsoleServerPortTemplate.objects.get(pk=id)
|
||||
console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field()
|
||||
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device(self, id: int) -> DeviceType:
|
||||
return models.Device.objects.get(pk=id)
|
||||
device: DeviceType = strawberry_django.field()
|
||||
device_list: List[DeviceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay(self, id: int) -> DeviceBayType:
|
||||
return models.DeviceBay.objects.get(pk=id)
|
||||
device_bay: DeviceBayType = strawberry_django.field()
|
||||
device_bay_list: List[DeviceBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay_template(self, id: int) -> DeviceBayTemplateType:
|
||||
return models.DeviceBayTemplate.objects.get(pk=id)
|
||||
device_bay_template: DeviceBayTemplateType = strawberry_django.field()
|
||||
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_role(self, id: int) -> DeviceRoleType:
|
||||
return models.DeviceRole.objects.get(pk=id)
|
||||
device_role: DeviceRoleType = strawberry_django.field()
|
||||
device_role_list: List[DeviceRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_type(self, id: int) -> DeviceTypeType:
|
||||
return models.DeviceType.objects.get(pk=id)
|
||||
device_type: DeviceTypeType = strawberry_django.field()
|
||||
device_type_list: List[DeviceTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port(self, id: int) -> FrontPortType:
|
||||
return models.FrontPort.objects.get(pk=id)
|
||||
front_port: FrontPortType = strawberry_django.field()
|
||||
front_port_list: List[FrontPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port_template(self, id: int) -> FrontPortTemplateType:
|
||||
return models.FrontPortTemplate.objects.get(pk=id)
|
||||
front_port_template: FrontPortTemplateType = strawberry_django.field()
|
||||
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface(self, id: int) -> InterfaceType:
|
||||
return models.Interface.objects.get(pk=id)
|
||||
interface: InterfaceType = strawberry_django.field()
|
||||
interface_list: List[InterfaceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface_template(self, id: int) -> InterfaceTemplateType:
|
||||
return models.InterfaceTemplate.objects.get(pk=id)
|
||||
interface_template: InterfaceTemplateType = strawberry_django.field()
|
||||
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item(self, id: int) -> InventoryItemType:
|
||||
return models.InventoryItem.objects.get(pk=id)
|
||||
inventory_item: InventoryItemType = strawberry_django.field()
|
||||
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_role(self, id: int) -> InventoryItemRoleType:
|
||||
return models.InventoryItemRole.objects.get(pk=id)
|
||||
inventory_item_role: InventoryItemRoleType = strawberry_django.field()
|
||||
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
|
||||
return models.InventoryItemTemplate.objects.get(pk=id)
|
||||
inventory_item_template: InventoryItemTemplateType = strawberry_django.field()
|
||||
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def location(self, id: int) -> LocationType:
|
||||
return models.Location.objects.get(pk=id)
|
||||
location: LocationType = strawberry_django.field()
|
||||
location_list: List[LocationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def manufacturer(self, id: int) -> ManufacturerType:
|
||||
return models.Manufacturer.objects.get(pk=id)
|
||||
manufacturer: ManufacturerType = strawberry_django.field()
|
||||
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module(self, id: int) -> ModuleType:
|
||||
return models.Module.objects.get(pk=id)
|
||||
module: ModuleType = strawberry_django.field()
|
||||
module_list: List[ModuleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay(self, id: int) -> ModuleBayType:
|
||||
return models.ModuleBay.objects.get(pk=id)
|
||||
module_bay: ModuleBayType = strawberry_django.field()
|
||||
module_bay_list: List[ModuleBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay_template(self, id: int) -> ModuleBayTemplateType:
|
||||
return models.ModuleBayTemplate.objects.get(pk=id)
|
||||
module_bay_template: ModuleBayTemplateType = strawberry_django.field()
|
||||
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_type(self, id: int) -> ModuleTypeType:
|
||||
return models.ModuleType.objects.get(pk=id)
|
||||
module_type: ModuleTypeType = strawberry_django.field()
|
||||
module_type_list: List[ModuleTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def platform(self, id: int) -> PlatformType:
|
||||
return models.Platform.objects.get(pk=id)
|
||||
platform: PlatformType = strawberry_django.field()
|
||||
platform_list: List[PlatformType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_feed(self, id: int) -> PowerFeedType:
|
||||
return models.PowerFeed.objects.get(pk=id)
|
||||
power_feed: PowerFeedType = strawberry_django.field()
|
||||
power_feed_list: List[PowerFeedType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet(self, id: int) -> PowerOutletType:
|
||||
return models.PowerOutlet.objects.get(pk=id)
|
||||
power_outlet: PowerOutletType = strawberry_django.field()
|
||||
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
|
||||
return models.PowerOutletTemplate.objects.get(pk=id)
|
||||
power_outlet_template: PowerOutletTemplateType = strawberry_django.field()
|
||||
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_panel(self, id: int) -> PowerPanelType:
|
||||
return models.PowerPanel.objects.get(id=id)
|
||||
power_panel: PowerPanelType = strawberry_django.field()
|
||||
power_panel_list: List[PowerPanelType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port(self, id: int) -> PowerPortType:
|
||||
return models.PowerPort.objects.get(id=id)
|
||||
power_port: PowerPortType = strawberry_django.field()
|
||||
power_port_list: List[PowerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port_template(self, id: int) -> PowerPortTemplateType:
|
||||
return models.PowerPortTemplate.objects.get(id=id)
|
||||
power_port_template: PowerPortTemplateType = strawberry_django.field()
|
||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_type(self, id: int) -> RackTypeType:
|
||||
return models.RackType.objects.get(id=id)
|
||||
rack_type: RackTypeType = strawberry_django.field()
|
||||
rack_type_list: List[RackTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack(self, id: int) -> RackType:
|
||||
return models.Rack.objects.get(id=id)
|
||||
rack: RackType = strawberry_django.field()
|
||||
rack_list: List[RackType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_reservation(self, id: int) -> RackReservationType:
|
||||
return models.RackReservation.objects.get(id=id)
|
||||
rack_reservation: RackReservationType = strawberry_django.field()
|
||||
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_role(self, id: int) -> RackRoleType:
|
||||
return models.RackRole.objects.get(id=id)
|
||||
rack_role: RackRoleType = strawberry_django.field()
|
||||
rack_role_list: List[RackRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port(self, id: int) -> RearPortType:
|
||||
return models.RearPort.objects.get(id=id)
|
||||
rear_port: RearPortType = strawberry_django.field()
|
||||
rear_port_list: List[RearPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port_template(self, id: int) -> RearPortTemplateType:
|
||||
return models.RearPortTemplate.objects.get(id=id)
|
||||
rear_port_template: RearPortTemplateType = strawberry_django.field()
|
||||
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def region(self, id: int) -> RegionType:
|
||||
return models.Region.objects.get(id=id)
|
||||
region: RegionType = strawberry_django.field()
|
||||
region_list: List[RegionType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site(self, id: int) -> SiteType:
|
||||
return models.Site.objects.get(id=id)
|
||||
site: SiteType = strawberry_django.field()
|
||||
site_list: List[SiteType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site_group(self, id: int) -> SiteGroupType:
|
||||
return models.SiteGroup.objects.get(id=id)
|
||||
site_group: SiteGroupType = strawberry_django.field()
|
||||
site_group_list: List[SiteGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_chassis(self, id: int) -> VirtualChassisType:
|
||||
return models.VirtualChassis.objects.get(id=id)
|
||||
virtual_chassis: VirtualChassisType = strawberry_django.field()
|
||||
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
|
||||
return models.VirtualDeviceContext.objects.get(id=id)
|
||||
virtual_device_context: VirtualDeviceContextType = strawberry_django.field()
|
||||
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()
|
||||
|
||||
@@ -11,7 +11,7 @@ import utilities.ordering
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0117_customfield_uniqueness'),
|
||||
('extras', '0118_customfield_uniqueness'),
|
||||
('dcim', '0187_alter_device_vc_position'),
|
||||
]
|
||||
|
||||
@@ -39,7 +39,7 @@ class Migration(migrations.Migration):
|
||||
)),
|
||||
('model', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('form_factor', models.CharField(blank=True, max_length=50)),
|
||||
('form_factor', models.CharField(max_length=50)),
|
||||
('width', models.PositiveSmallIntegerField(default=19)),
|
||||
('u_height', models.PositiveSmallIntegerField(
|
||||
default=42,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-25 07:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -20,9 +18,4 @@ class Migration(migrations.Migration):
|
||||
name='airflow',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='racktype',
|
||||
name='airflow',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -6,8 +6,8 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0189_moduletype_airflow_rack_airflow_racktype_airflow'),
|
||||
('extras', '0120_customfield_related_object_filter'),
|
||||
('dcim', '0189_moduletype_rack_airflow'),
|
||||
('extras', '0121_customfield_related_object_filter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
26
netbox/dcim/migrations/0191_module_bay_rebuild.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations
|
||||
import mptt
|
||||
import mptt.managers
|
||||
|
||||
|
||||
def rebuild_mptt(apps, schema_editor):
|
||||
manager = mptt.managers.TreeManager()
|
||||
ModuleBay = apps.get_model('dcim', 'ModuleBay')
|
||||
manager.model = ModuleBay
|
||||
mptt.register(ModuleBay)
|
||||
manager.contribute_to_class(ModuleBay, 'objects')
|
||||
manager.rebuild()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0190_nested_modules'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=rebuild_mptt,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -164,7 +164,7 @@ class Cable(PrimaryModel):
|
||||
if self.length is not None and not self.length_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a cable length"))
|
||||
|
||||
if self.pk is None and (not self.a_terminations or not self.b_terminations):
|
||||
if self._state.adding and (not self.a_terminations or not self.b_terminations):
|
||||
raise ValidationError(_("Must define A and B terminations when creating a new cable."))
|
||||
|
||||
if self._terminations_modified:
|
||||
@@ -366,11 +366,11 @@ class CableTermination(ChangeLoggedModel):
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# Delete the cable association on the terminating object
|
||||
termination_model = self.termination._meta.model
|
||||
termination_model.objects.filter(pk=self.termination_id).update(
|
||||
cable=None,
|
||||
cable_end=''
|
||||
)
|
||||
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
|
||||
termination.snapshot()
|
||||
termination.cable = None
|
||||
termination.cable_end = ''
|
||||
termination.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.pk is not None and self._original_device_type != self.device_type_id:
|
||||
if not self._state.adding and self._original_device_type != self.device_type_id:
|
||||
raise ValidationError({
|
||||
"device_type": _("Component templates cannot be moved to a different device type.")
|
||||
})
|
||||
|
||||
@@ -561,7 +561,7 @@ class BaseInterface(models.Model):
|
||||
self.untagged_vlan = None
|
||||
|
||||
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
|
||||
if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
|
||||
if not self._state.adding and self.mode != InterfaceModeChoices.MODE_TAGGED:
|
||||
self.tagged_vlans.clear()
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
@@ -1072,7 +1072,7 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
super().clean()
|
||||
|
||||
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
frontport_count = self.frontports.count()
|
||||
if self.positions < frontport_count:
|
||||
raise ValidationError({
|
||||
@@ -1314,7 +1314,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
||||
})
|
||||
|
||||
# Validation for moving InventoryItems
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
# Cannot move an InventoryItem to another device if it has a parent
|
||||
if self.parent and self.parent.device != self.device:
|
||||
raise ValidationError({
|
||||
|
||||
@@ -293,7 +293,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
|
||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
|
||||
if self.pk and self.u_height > self._original_u_height:
|
||||
if not self._state.adding and self.u_height > self._original_u_height:
|
||||
for d in Device.objects.filter(device_type=self, position__isnull=False):
|
||||
face_required = None if self.is_full_depth else d.face
|
||||
u_available = d.rack.get_available_units(
|
||||
@@ -310,7 +310,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
})
|
||||
|
||||
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
|
||||
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
|
||||
elif not self._state.adding and self._original_u_height > 0 and self.u_height == 0:
|
||||
racked_instance_count = Device.objects.filter(
|
||||
device_type=self,
|
||||
position__isnull=False
|
||||
@@ -1351,7 +1351,7 @@ class VirtualChassis(PrimaryModel):
|
||||
|
||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||
# VirtualChassis.)
|
||||
if self.pk and self.master and self.master not in self.members.all():
|
||||
if not self._state.adding and self.master and self.master not in self.members.all():
|
||||
raise ValidationError({
|
||||
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
|
||||
master=self.master
|
||||
|
||||
@@ -41,24 +41,12 @@ class RackBase(WeightMixin, PrimaryModel):
|
||||
"""
|
||||
Base class for RackType & Rack. Holds
|
||||
"""
|
||||
form_factor = models.CharField(
|
||||
choices=RackFormFactorChoices,
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('form factor')
|
||||
)
|
||||
width = models.PositiveSmallIntegerField(
|
||||
choices=RackWidthChoices,
|
||||
default=RackWidthChoices.WIDTH_19IN,
|
||||
verbose_name=_('width'),
|
||||
help_text=_('Rail-to-rail width')
|
||||
)
|
||||
airflow = models.CharField(
|
||||
verbose_name=_('airflow'),
|
||||
max_length=50,
|
||||
choices=RackAirflowChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Numbering
|
||||
u_height = models.PositiveSmallIntegerField(
|
||||
@@ -131,6 +119,11 @@ class RackType(RackBase):
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
form_factor = models.CharField(
|
||||
choices=RackFormFactorChoices,
|
||||
max_length=50,
|
||||
verbose_name=_('form factor')
|
||||
)
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -147,7 +140,7 @@ class RackType(RackBase):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'manufacturer', 'form_factor', 'width', 'u_height', 'airflow', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
)
|
||||
prerequisite_models = (
|
||||
@@ -248,10 +241,16 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
"""
|
||||
# Fields which cannot be set locally if a RackType is assigned
|
||||
RACKTYPE_FIELDS = (
|
||||
'form_factor', 'width', 'airflow', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
|
||||
)
|
||||
|
||||
form_factor = models.CharField(
|
||||
choices=RackFormFactorChoices,
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('form factor')
|
||||
)
|
||||
rack_type = models.ForeignKey(
|
||||
to='dcim.RackType',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -321,6 +320,12 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
verbose_name=_('asset tag'),
|
||||
help_text=_('A unique tag used to identify this rack')
|
||||
)
|
||||
airflow = models.CharField(
|
||||
verbose_name=_('airflow'),
|
||||
max_length=50,
|
||||
choices=RackAirflowChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
@@ -377,7 +382,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
if self.max_weight and not self.weight_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a maximum weight"))
|
||||
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
|
||||
|
||||
# Validate that Rack is tall enough to house the highest mounted Device
|
||||
@@ -463,7 +468,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
}
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
if not self._state.adding:
|
||||
|
||||
# Retrieve all devices installed within the rack
|
||||
devices = Device.objects.prefetch_related(
|
||||
|
||||
@@ -290,6 +290,11 @@ class DeviceComponentTable(NetBoxTable):
|
||||
linkify=True,
|
||||
order_by=('_name',)
|
||||
)
|
||||
device_status = columns.ChoiceFieldColumn(
|
||||
accessor=tables.A('device__status'),
|
||||
verbose_name=_('Device Status'),
|
||||
color=lambda x: x.device.get_status_color(),
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
@@ -679,7 +684,8 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'data-virtual': lambda record: "true" if record.is_virtual else "false",
|
||||
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
|
||||
'data-cable-status': lambda record: record.cable.status if record.cable else "",
|
||||
'data-type': lambda record: record.type
|
||||
'data-type': lambda record: record.type,
|
||||
'data-connected': lambda record: "connected" if record.mark_connected or record.cable else "disconnected"
|
||||
}
|
||||
|
||||
|
||||
@@ -898,12 +904,10 @@ class ModuleBayTable(ModularDeviceComponentTable):
|
||||
|
||||
|
||||
class DeviceModuleBayTable(ModuleBayTable):
|
||||
name = tables.TemplateColumn(
|
||||
name = columns.MPTTColumn(
|
||||
verbose_name=_('Name'),
|
||||
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
||||
'{{ value }}</a>',
|
||||
order_by=Accessor('_name'),
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
linkify=True,
|
||||
order_by=Accessor('_name')
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
extra_buttons=MODULEBAY_BUTTONS
|
||||
@@ -1048,7 +1052,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
|
||||
)
|
||||
device = tables.TemplateColumn(
|
||||
verbose_name=_('Device'),
|
||||
order_by=('_name',),
|
||||
order_by=('device___name',),
|
||||
template_code=DEVICE_LINK,
|
||||
linkify=True
|
||||
)
|
||||
|
||||
@@ -99,6 +99,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
)
|
||||
device_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('Devices')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
|
||||
@@ -192,6 +192,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_site',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -277,6 +278,7 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'new description',
|
||||
}
|
||||
user_permissions = ('dcim.view_manufacturer',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -287,9 +289,9 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
rack_types = (
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 1', slug='rack-type-1'),
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 2', slug='rack-type-2'),
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 3', slug='rack-type-3'),
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
RackType(manufacturer=manufacturers[0], model='Rack Type 3', slug='rack-type-3', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
)
|
||||
RackType.objects.bulk_create(rack_types)
|
||||
|
||||
@@ -298,16 +300,19 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Rack Type 4',
|
||||
'slug': 'rack-type-4',
|
||||
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Rack Type 5',
|
||||
'slug': 'rack-type-5',
|
||||
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Rack Type 6',
|
||||
'slug': 'rack-type-6',
|
||||
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -318,6 +323,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
user_permissions = ('dcim.view_site', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -406,6 +412,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_rack', 'users.view_user')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -485,6 +492,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'part_number': 'ABC123',
|
||||
}
|
||||
user_permissions = ('dcim.view_manufacturer', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -530,6 +538,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'part_number': 'ABC123',
|
||||
}
|
||||
user_permissions = ('dcim.view_manufacturer', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -701,6 +710,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_devicetype', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -806,6 +816,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_rearporttemplate', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -943,6 +954,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_devicetype', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -983,6 +995,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_devicetype', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1023,6 +1036,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1141,6 +1155,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'status': 'failed',
|
||||
}
|
||||
user_permissions = (
|
||||
'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
|
||||
'extras.view_configtemplate',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1331,6 +1349,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'serial': '1234ABCD',
|
||||
}
|
||||
user_permissions = ('dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_device')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1397,6 +1416,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = ConsoleServerPort
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1439,6 +1459,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = ConsolePort
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1481,6 +1502,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = PowerOutlet
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1520,6 +1542,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = PowerPort
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1568,6 +1591,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = Interface
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1702,6 +1726,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = Interface
|
||||
user_permissions = ('dcim.view_device', 'dcim.view_rearport')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1760,6 +1785,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
'description': 'New description',
|
||||
}
|
||||
peer_termination_type = Interface
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1801,6 +1827,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1841,6 +1868,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_device', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1904,6 +1932,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
user_permissions = ('dcim.view_device', 'dcim.view_manufacturer')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2200,6 +2229,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPanel
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
|
||||
user_permissions = ('dcim.view_site', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2252,6 +2282,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
user_permissions = ('dcim.view_powerpanel', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -37,6 +37,10 @@ class DeviceComponentFilterSetTests:
|
||||
params = {'device_role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device_status(self):
|
||||
params = {'device_status': ['active', 'planned']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceComponentTemplateFilterSetTests:
|
||||
|
||||
@@ -496,7 +500,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
weight=10,
|
||||
max_weight=1000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar1'
|
||||
description='foobar1',
|
||||
),
|
||||
RackType(
|
||||
manufacturer=manufacturers[1],
|
||||
@@ -514,7 +518,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
weight=20,
|
||||
max_weight=2000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar2'
|
||||
description='foobar2',
|
||||
),
|
||||
RackType(
|
||||
manufacturer=manufacturers[2],
|
||||
@@ -741,7 +745,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
weight=10,
|
||||
max_weight=1000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar1'
|
||||
description='foobar1',
|
||||
airflow=RackAirflowChoices.FRONT_TO_REAR
|
||||
),
|
||||
Rack(
|
||||
name='Rack 2',
|
||||
@@ -763,7 +768,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
weight=20,
|
||||
max_weight=2000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar2'
|
||||
description='foobar2',
|
||||
airflow=RackAirflowChoices.REAR_TO_FRONT
|
||||
),
|
||||
Rack(
|
||||
name='Rack 3',
|
||||
@@ -954,6 +960,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'rack_type': [rack_types[0].slug, rack_types[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_airflow(self):
|
||||
params = {'airflow': RackAirflowChoices.FRONT_TO_REAR}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackReservation.objects.all()
|
||||
@@ -1376,7 +1386,8 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
part_number='Part Number 1',
|
||||
weight=10,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar1'
|
||||
description='foobar1',
|
||||
airflow=ModuleAirflowChoices.FRONT_TO_REAR
|
||||
),
|
||||
ModuleType(
|
||||
manufacturer=manufacturers[1],
|
||||
@@ -1384,7 +1395,8 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
part_number='Part Number 2',
|
||||
weight=20,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar2'
|
||||
description='foobar2',
|
||||
airflow=ModuleAirflowChoices.REAR_TO_FRONT
|
||||
),
|
||||
ModuleType(
|
||||
manufacturer=manufacturers[2],
|
||||
@@ -1495,6 +1507,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_airflow(self):
|
||||
params = {'airflow': RackAirflowChoices.FRONT_TO_REAR}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ConsolePortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
@@ -2825,10 +2841,10 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3006,10 +3022,10 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3187,10 +3203,10 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3376,10 +3392,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3575,7 +3591,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[0],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=1,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='active',
|
||||
),
|
||||
Device(
|
||||
name='Device 1B',
|
||||
@@ -3586,7 +3603,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[2],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=2,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='planned',
|
||||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
@@ -3594,7 +3612,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
location=locations[1],
|
||||
rack=racks[1]
|
||||
rack=racks[1],
|
||||
status='offline',
|
||||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
@@ -3602,14 +3621,16 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
rack=racks[2]
|
||||
rack=racks[2],
|
||||
status='offline',
|
||||
),
|
||||
# For cable connections
|
||||
Device(
|
||||
name=None,
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[3]
|
||||
site=sites[3],
|
||||
status='offline',
|
||||
),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -4056,10 +4077,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4246,10 +4267,10 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4428,9 +4449,9 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -4576,9 +4597,9 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -5226,6 +5247,10 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_type(self):
|
||||
params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'type__empty': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
params = {'type__empty': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [LinkStatusChoices.STATUS_CONNECTED]}
|
||||
|
||||
@@ -346,9 +346,9 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
rack_types = (
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 1', slug='rack-type-1',),
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 2', slug='rack-type-2',),
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 3', slug='rack-type-3',),
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
RackType(manufacturer=manufacturers[0], model='RackType 3', slug='rack-type-3', form_factor=RackFormFactorChoices.TYPE_CABINET,),
|
||||
)
|
||||
RackType.objects.bulk_create(rack_types)
|
||||
|
||||
@@ -369,6 +369,7 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'weight': 100,
|
||||
'max_weight': 2000,
|
||||
'weight_unit': WeightUnitChoices.UNIT_POUND,
|
||||
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||
'comments': 'Some comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
@@ -380,7 +380,9 @@ class SiteGroupContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class SiteListView(generic.ObjectListView):
|
||||
queryset = Site.objects.all()
|
||||
queryset = Site.objects.annotate(
|
||||
device_count=count_related(Device, 'site')
|
||||
)
|
||||
filterset = filtersets.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
@@ -2126,7 +2128,7 @@ class DeviceRenderConfigView(generic.ObjectView):
|
||||
try:
|
||||
rendered_config = config_template.render(context=context_data)
|
||||
except TemplateError as e:
|
||||
messages.error(request, f"An error occurred while rendering the template: {e}")
|
||||
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
|
||||
rendered_config = traceback.format_exc()
|
||||
|
||||
return {
|
||||
@@ -2890,7 +2892,13 @@ class DeviceBayPopulateView(generic.ObjectEditView):
|
||||
device_bay.snapshot()
|
||||
device_bay.installed_device = form.cleaned_data['installed_device']
|
||||
device_bay.save()
|
||||
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||
messages.success(
|
||||
request,
|
||||
_("Installed device {device} in bay {device_bay}.").format(
|
||||
device=device_bay.installed_device,
|
||||
device_bay=device_bay
|
||||
)
|
||||
)
|
||||
return_url = self.get_return_url(request)
|
||||
|
||||
return redirect(return_url)
|
||||
@@ -2925,7 +2933,13 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
|
||||
removed_device = device_bay.installed_device
|
||||
device_bay.installed_device = None
|
||||
device_bay.save()
|
||||
messages.success(request, f"{removed_device} has been removed from {device_bay}.")
|
||||
messages.success(
|
||||
request,
|
||||
_("Removed device {device} from bay {device_bay}.").format(
|
||||
device=removed_device,
|
||||
device_bay=device_bay
|
||||
)
|
||||
)
|
||||
return_url = self.get_return_url(request, device_bay.device)
|
||||
|
||||
return redirect(return_url)
|
||||
@@ -3239,10 +3253,10 @@ class CableEditView(generic.ObjectEditView):
|
||||
doesn't currently provide a hook for dynamic class resolution.
|
||||
"""
|
||||
a_terminations_type = CABLE_TERMINATION_TYPES.get(
|
||||
request.GET.get('a_terminations_type') or request.POST.get('a_terminations_type')
|
||||
request.POST.get('a_terminations_type') or request.GET.get('a_terminations_type')
|
||||
)
|
||||
b_terminations_type = CABLE_TERMINATION_TYPES.get(
|
||||
request.GET.get('b_terminations_type') or request.POST.get('b_terminations_type')
|
||||
request.POST.get('b_terminations_type') or request.GET.get('b_terminations_type')
|
||||
)
|
||||
|
||||
if obj.pk:
|
||||
@@ -3493,7 +3507,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
||||
|
||||
membership_form.save()
|
||||
messages.success(request, mark_safe(
|
||||
f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||
_('Added member <a href="{url}">{device}</a>').format(url=device.get_absolute_url(), device=escape(device))
|
||||
))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
@@ -3538,7 +3552,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
||||
# Protect master device from being removed
|
||||
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
||||
if virtual_chassis is not None:
|
||||
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
|
||||
messages.error(
|
||||
request,
|
||||
_('Unable to remove master device {device} from the virtual chassis.').format(device=device)
|
||||
)
|
||||
return redirect(device.get_absolute_url())
|
||||
|
||||
if form.is_valid():
|
||||
@@ -3550,7 +3567,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
||||
device.vc_priority = None
|
||||
device.save()
|
||||
|
||||
msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
|
||||
msg = _('Removed {device} from virtual chassis {chassis}').format(
|
||||
device=device,
|
||||
chassis=device.virtual_chassis
|
||||
)
|
||||
messages.success(request, msg)
|
||||
|
||||
return redirect(self.get_return_url(request, device))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras import models
|
||||
@@ -20,6 +22,12 @@ __all__ = [
|
||||
'NestedWebhookSerializer',
|
||||
]
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
class NestedEventRuleSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ from .serializers_.configtemplates import *
|
||||
from .serializers_.savedfilters import *
|
||||
from .serializers_.scripts import *
|
||||
from .serializers_.tags import *
|
||||
from .nested_serializers import *
|
||||
|
||||