Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6452b33d8 | ||
|
|
16917133b2 | ||
|
|
28eada13d3 | ||
|
|
6ddd3cc779 | ||
|
|
1a631dd7cc | ||
|
|
8c07978042 | ||
|
|
7e3d8e9c3b | ||
|
|
e396097f3c | ||
|
|
8d6cec408c | ||
|
|
e7fcbffaf3 | ||
|
|
0b9ead3e8b | ||
|
|
c0fec28b2a | ||
|
|
382e246b2c | ||
|
|
fff4ec78ad | ||
|
|
8951aa815f | ||
|
|
b89601d93d | ||
|
|
e63fe23af8 | ||
|
|
2da1a754c4 | ||
|
|
abfa28dc56 | ||
|
|
8e427e57ea | ||
|
|
dbaa9c1ce1 | ||
|
|
bd5e7a8d1a | ||
|
|
a15ff294dd | ||
|
|
26f8c3aae3 | ||
|
|
cc51e7032b | ||
|
|
001f06cc9a | ||
|
|
4017d0ca35 | ||
|
|
21962b3488 | ||
|
|
7a92c20576 | ||
|
|
3326a6543c | ||
|
|
674af4d6bc | ||
|
|
8c9bb73ff7 | ||
|
|
327ad8cfc9 | ||
|
|
1e845e6b46 | ||
|
|
b4265b74f4 | ||
|
|
954b5e9ddf | ||
|
|
d122c334fd | ||
|
|
24b76792a9 | ||
|
|
9c7f2ec98c | ||
|
|
3b3990a4e6 | ||
|
|
6fb476081e | ||
|
|
4f7bfc836c | ||
|
|
b40ffcccb9 | ||
|
|
1e5d19927a | ||
|
|
09a0e579fa | ||
|
|
9ccbb08e29 | ||
|
|
eb645ee900 | ||
|
|
e36f23ed03 | ||
|
|
13bd2ed767 | ||
|
|
0ff0edd477 | ||
|
|
05daa16aed | ||
|
|
256d69d08b | ||
|
|
9c532c7d89 | ||
|
|
c34fea6c9b | ||
|
|
2ed0534117 | ||
|
|
954e29aec3 | ||
|
|
494d410847 | ||
|
|
fe0ae39903 | ||
|
|
f873735dd4 | ||
|
|
6035ad139a | ||
|
|
27d15615b3 | ||
|
|
81f00fd03a | ||
|
|
58bc388457 | ||
|
|
74315080a3 | ||
|
|
7580aa0781 | ||
|
|
4ca2b21a70 | ||
|
|
1e5f79a8ed | ||
|
|
f00a93c066 | ||
|
|
5f94dff815 | ||
|
|
576498955f | ||
|
|
58d9057ccd | ||
|
|
813347121e | ||
|
|
c383086aac | ||
|
|
dba6e532c4 | ||
|
|
f0eb8b9c64 | ||
|
|
f56843333d | ||
|
|
8279eaff5b | ||
|
|
ca210168df | ||
|
|
476194f0aa | ||
|
|
69e1394fef | ||
|
|
ac12eae0b7 | ||
|
|
ce67d2c13b | ||
|
|
97eb5bda50 | ||
|
|
6251296776 | ||
|
|
5940f5fa61 | ||
|
|
bb06b733c4 | ||
|
|
1c4a1e075d | ||
|
|
a2cd4d0983 | ||
|
|
e13bc0694d | ||
|
|
d8c5147e02 | ||
|
|
ac9f561372 | ||
|
|
5ddbacaa1f | ||
|
|
e6f41f73f7 | ||
|
|
110b2b3d97 | ||
|
|
6a316df787 | ||
|
|
9f7743e5da | ||
|
|
33bc1320c4 | ||
|
|
27a39339df | ||
|
|
81108e405f | ||
|
|
82de559317 | ||
|
|
532dbabbab | ||
|
|
e8e95f5e97 | ||
|
|
aa3f4cb5f5 | ||
|
|
35307d213f | ||
|
|
e7bd0e53d7 | ||
|
|
dbc52dc6c7 | ||
|
|
4deb6e5968 | ||
|
|
d2cbdfe7d7 | ||
|
|
5c5a53bf3f | ||
|
|
75225c6c75 | ||
|
|
aab96565f2 | ||
|
|
ba4b5fed0b | ||
|
|
111a1ad888 | ||
|
|
55fad2f533 | ||
|
|
dfce55ceff | ||
|
|
fcc498641f | ||
|
|
9a655d80e1 | ||
|
|
a964645c0a | ||
|
|
7ac6dff96d | ||
|
|
1e6f222475 | ||
|
|
4e763462e6 | ||
|
|
e59f776e02 | ||
|
|
e3c3ca191c | ||
|
|
8e636c5427 | ||
|
|
f851bd80b9 | ||
|
|
ec89a9b106 | ||
|
|
2172ddde61 | ||
|
|
23e6534060 | ||
|
|
ccb2480e98 | ||
|
|
ebd6c59934 | ||
|
|
2fd23f35c8 | ||
|
|
364826d2d8 | ||
|
|
4b6e8a9e75 | ||
|
|
66d792e0d8 | ||
|
|
74727786c1 | ||
|
|
8e802abf0d | ||
|
|
fec0badd5a | ||
|
|
ce04ec20e8 | ||
|
|
dda7837069 | ||
|
|
bfcae8088d | ||
|
|
f11dc00fae | ||
|
|
648aeaaf14 | ||
|
|
3c36549ff1 | ||
|
|
6d5af67da8 | ||
|
|
1bfb6e6f34 | ||
|
|
0cf8264c0e | ||
|
|
1e03eb4eb8 | ||
|
|
3b8a3dc66a | ||
|
|
30e67047d3 | ||
|
|
7de5efda2a | ||
|
|
8715f6fe87 |
16
.github/ISSUE_TEMPLATE/01-feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.1.3
|
||||
placeholder: v4.1.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -24,6 +24,20 @@ body:
|
||||
- Data model extension
|
||||
- New functionality
|
||||
- Change to existing functionality
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- N/A
|
||||
default: 2
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -22,11 +22,24 @@ body:
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Triage priority
|
||||
description: >
|
||||
Issue triage may be prioritized in some cases. Select whichever of the following
|
||||
conditions applies, if any.
|
||||
options:
|
||||
- I volunteer to perform this work (if approved)
|
||||
- I'm a NetBox Labs customer
|
||||
- N/A
|
||||
default: 2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.1.3
|
||||
placeholder: v4.1.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,6 +7,9 @@ contact_links:
|
||||
- name: ❓ Discussion
|
||||
url: https://github.com/netbox-community/netbox/discussions
|
||||
about: "If you're just looking for help, try starting a discussion instead."
|
||||
- name: 👔 Professional Support
|
||||
url: https://netboxlabs.com/netbox-enterprise/
|
||||
about: "Professional support is available for NetBox Enterprise or Cloud."
|
||||
- name: 🌎 Correct a Translation
|
||||
url: https://explore.transifex.com/netbox-community/netbox/
|
||||
about: "Spot an incorrect translation? You can propose a fix on Transifex."
|
||||
|
||||
9
.github/workflows/ci.yml
vendored
@@ -15,6 +15,11 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Add concurrency group to control job running
|
||||
concurrency:
|
||||
group: ${{ github.event_name }}-${{ github.ref }}-${{ github.actor }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -73,7 +78,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage tblib
|
||||
pip install ruff coverage tblib
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
@@ -85,7 +90,7 @@ jobs:
|
||||
run: python netbox/manage.py makemigrations --check
|
||||
|
||||
- name: Check PEP8 compliance
|
||||
run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
|
||||
run: ruff check netbox/
|
||||
|
||||
- name: Check UI ESLint, TypeScript, and Prettier Compliance
|
||||
run: yarn --cwd netbox/project-static validate
|
||||
|
||||
@@ -18,8 +18,17 @@ jobs:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
|
||||
steps:
|
||||
- name: Create app token
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: 1076524
|
||||
private-key: ${{ secrets.HOUSEKEEPING_SECRET_KEY }}
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
44
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Ruff linter"
|
||||
args: [ netbox/ ]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: django-check
|
||||
name: "Django system check"
|
||||
description: "Run Django's internal check for common problems"
|
||||
entry: python netbox/manage.py check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: django-makemigrations
|
||||
name: "Django migrations check"
|
||||
description: "Check for any missing Django migrations"
|
||||
entry: python netbox/manage.py makemigrations --check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: mkdocs-build
|
||||
name: "Build documentation"
|
||||
description: "Build the documentation with mkdocs"
|
||||
files: 'docs/'
|
||||
entry: mkdocs build
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: yarn-validate
|
||||
name: "Yarn validate"
|
||||
description: "Check UI ESLint, TypeScript, and Prettier compliance"
|
||||
files: 'netbox/project-static/'
|
||||
entry: yarn --cwd netbox/project-static validate
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: verify-bundles
|
||||
name: "Verify static asset bundles"
|
||||
description: "Ensure that any modified static assets have been compiled"
|
||||
files: 'netbox/project-static/'
|
||||
entry: scripts/verify-bundles.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
12
.tx/config
Executable file
@@ -0,0 +1,12 @@
|
||||
[main]
|
||||
host = https://app.transifex.com
|
||||
|
||||
[o:netbox-community:p:netbox:r:9cbf4fcf95b3d92e4ebbf1a5e5d1caee]
|
||||
file_filter = netbox/translations/<lang>/LC_MESSAGES/django.po
|
||||
source_file = netbox/translations/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
minimum_perc = 0
|
||||
resource_name = django.po
|
||||
replace_edited_strings = false
|
||||
keep_translations = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
|
||||
<p><strong>The cornerstone of every automated network</strong></p>
|
||||
<a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
|
||||
@@ -116,6 +116,10 @@ PyYAML
|
||||
# https://github.com/psf/requests/blob/main/HISTORY.md
|
||||
requests
|
||||
|
||||
# rq
|
||||
# https://github.com/rq/rq/blob/master/CHANGES.md
|
||||
rq
|
||||
|
||||
# Social authentication framework
|
||||
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
|
||||
social-auth-core
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"left-to-right",
|
||||
"right-to-left",
|
||||
"side-to-rear",
|
||||
"rear-to-side",
|
||||
"bottom-to-top",
|
||||
"top-to-bottom",
|
||||
"passive",
|
||||
"mixed"
|
||||
]
|
||||
@@ -326,6 +329,7 @@
|
||||
"100base-tx",
|
||||
"100base-t1",
|
||||
"1000base-t",
|
||||
"1000base-lx",
|
||||
"1000base-tx",
|
||||
"2.5gbase-t",
|
||||
"5gbase-t",
|
||||
|
||||
52
docs/administration/authentication/google.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Google
|
||||
|
||||
This guide explains how to configure single sign-on (SSO) support for NetBox using [Google OAuth2](https://developers.google.com/identity/protocols/oauth2/web-server) as an authentication backend.
|
||||
|
||||
## Google OAuth2 Configuration
|
||||
|
||||
1. Log into [console.cloud.google.com](https://console.cloud.google.com/).
|
||||
2. Create new project for NetBox.
|
||||
3. Under "APIs and Services" click "OAuth consent screen" and enter the required information.
|
||||
4. Under "Credentials," click "Create Credentials" and select "OAuth 2.0 Client ID." Select type "Web application."
|
||||
- "Authorized JavaScript origins" should follow the format `http[s]://<netbox>[:<port>]`
|
||||
- "Authorized redirect URIs" should follow the format `http[s]://<netbox>[:<port>]/oauth/complete/google-oauth2/`
|
||||
5. Copy the "Client ID" and "Client Secret" values somewhere convenient.
|
||||
|
||||
!!! note
|
||||
Google requires the NetBox hostname to use a public top-level-domain (e.g. `.com`, `.net`). The use of IP addresses is not permitted (except `127.0.0.1`).
|
||||
|
||||
For more information, consult [Google's documentation](https://developers.google.com/identity/protocols/oauth2/web-server#prerequisites).
|
||||
|
||||
## NetBox Configuration
|
||||
|
||||
### 1. Enter configuration parameters
|
||||
|
||||
Enter the following configuration parameters in `configuration.py`, substituting your own values:
|
||||
|
||||
```python
|
||||
REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '{CLIENT_ID}'
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '{CLIENT_SECRET}'
|
||||
```
|
||||
|
||||
### 2. Restart NetBox
|
||||
|
||||
Restart the NetBox services so that the new configuration takes effect. This is typically done with the command below:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Log out of NetBox if already authenticated, and click the "Log In" button at top right. You should see the normal login form as well as an option to authenticate using Google. Click that link.
|
||||
|
||||

|
||||
|
||||
You should be redirected to Google's authentication portal. Enter the username/email and password of your test account to continue. You may also be prompted to grant this application access to your account.
|
||||
|
||||

|
||||
|
||||
If successful, you will be redirected back to the NetBox UI, and will be logged in as the Google user. You can verify this by navigating to your profile (using the button at top right).
|
||||
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions.
|
||||
@@ -1,8 +1,8 @@
|
||||
# Microsoft Azure AD
|
||||
# Microsoft Entra ID
|
||||
|
||||
This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Azure Active Directory (AD)](https://azure.microsoft.com/en-us/services/active-directory/) as an authentication backend.
|
||||
This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) as an authentication backend.
|
||||
|
||||
## Azure AD Configuration
|
||||
## Entra ID Configuration
|
||||
|
||||
### 1. Create a test user (optional)
|
||||
|
||||
@@ -72,6 +72,9 @@ script_order = (MyCustomScript, AnotherCustomScript)
|
||||
|
||||
Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged.
|
||||
|
||||
!!! warning
|
||||
These are also defined and used as properties on the base custom script class, so don't use the same names as variables or override them in your custom script.
|
||||
|
||||
### `name`
|
||||
|
||||
This is the human-friendly names of your script. If omitted, the class name will be used.
|
||||
|
||||
@@ -49,6 +49,10 @@ This key lists all models which have been registered in NetBox which are not des
|
||||
|
||||
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
|
||||
|
||||
### `request_processors`
|
||||
|
||||
A list of context managers to invoke when processing a request e.g. in middleware or when executing a background job. Request processors can be registered with the `@register_request_processor` decorator.
|
||||
|
||||
### `search`
|
||||
|
||||
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
|
||||
|
||||
@@ -62,22 +62,7 @@ $issue-$description
|
||||
|
||||
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
|
||||
|
||||
### 3. Enable Pre-Commit Hooks
|
||||
|
||||
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
|
||||
|
||||
```no-highlight
|
||||
cd .git/hooks/
|
||||
ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
For the pre-commit hooks to work, you will also need to install the pycodestyle package:
|
||||
|
||||
```no-highlight
|
||||
python -m pip install pycodestyle
|
||||
```
|
||||
...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
### 4. Create a Python Virtual Environment
|
||||
### 3. Create a Python Virtual Environment
|
||||
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
|
||||
@@ -101,7 +86,7 @@ source ~/.venv/netbox/bin/activate
|
||||
|
||||
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
|
||||
|
||||
### 5. Install Required Packages
|
||||
### 4. Install Required Packages
|
||||
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
|
||||
|
||||
@@ -109,6 +94,26 @@ With the virtual environment activated, install the project's required Python pa
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 5. Install Pre-Commit
|
||||
|
||||
NetBox uses [`pre-commit`](https://pre-commit.com/) to automatically validate code when commiting new changes. This includes the following operations:
|
||||
|
||||
* Run the `ruff` Python linter
|
||||
* Run Django's internal system check
|
||||
* Check for missing database migrations
|
||||
* Validate any changes to the documentation with `mkdocs`
|
||||
* Validate Typescript & Sass styling with `yarn`
|
||||
* Ensure that any modified static front end assets have been recompiled
|
||||
|
||||
Enable `pre-commit` with the following commands _prior_ to commiting any changes:
|
||||
|
||||
```no-highlight
|
||||
python -m pip install ruff pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
You may also need to set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md).
|
||||
|
||||
### 6. Configure NetBox
|
||||
|
||||
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
|
||||
|
||||
@@ -90,7 +90,20 @@ This will automatically update the schema file at `contrib/generated_schema.json
|
||||
|
||||
### Update & Compile Translations
|
||||
|
||||
Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. Follow the documented process for [updating translated strings](./translations.md#updating-translated-strings) to do this.
|
||||
Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. First, retrieve any updated translation files using the Transifex CLI client:
|
||||
|
||||
```no-highlight
|
||||
tx pull
|
||||
```
|
||||
|
||||
Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
```no-highlight
|
||||
./manage.py compilemessages
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Consult the translation documentation for more detail on [updating translated strings](./translations.md#updating-translated-strings) if you've not set up the Transifex client already.
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [ruff](https://docs.astral.sh/ruff/) is used for linting (with certain [exceptions](#linter-exceptions)).
|
||||
|
||||
## Code
|
||||
|
||||
@@ -20,32 +20,32 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
|
||||
### PEP 8 Exceptions
|
||||
### Linting
|
||||
|
||||
NetBox ignores certain PEP8 assertions. These are listed below.
|
||||
The [ruff](https://docs.astral.sh/ruff/) linter is used 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 `ruff` manually, run:
|
||||
|
||||
#### Wildcard Imports
|
||||
```
|
||||
ruff check netbox/
|
||||
```
|
||||
|
||||
#### Linter Exceptions
|
||||
|
||||
The following rules are ignored when linting.
|
||||
|
||||
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
|
||||
|
||||
NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
|
||||
|
||||
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
|
||||
#### Maximum Line Length (E501)
|
||||
##### [F405](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage/): Undefined local with import star usage
|
||||
|
||||
NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
#### Line Breaks Following Binary Operators (W504)
|
||||
|
||||
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#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
The justification for ignoring this rule is the same as F403 above.
|
||||
|
||||
### Introducing New Dependencies
|
||||
|
||||
@@ -76,4 +76,4 @@ When adding a new dependency, a short description of the package and the URL of
|
||||
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. but never "Netbox" or any other deviation.
|
||||
|
||||
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.
|
||||
* There are SVG forms of the NetBox logo for both [light mode](../netbox_logo_light.svg) and [dark mode](../netbox_logo_dark.svg) available. It is preferred to use the SVG logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the desired size.
|
||||
|
||||
@@ -16,26 +16,31 @@ To update the English `.po` file from which all translations are derived, use th
|
||||
|
||||
Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
|
||||
|
||||
!!! note
|
||||
It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/develop/.github/workflows/update-translation-strings.yml).
|
||||
|
||||
## Updating Translated Strings
|
||||
|
||||
Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
|
||||
|
||||
Check the Transifex dashboard for languages that are not marked _ready for use_, being sure to click _Show all languages_ if it appears at the bottom of the list. Use machine translation to round out any not-ready languages. It's not necessary to review the machine translation immediately as the translation teams will handle that aspect; the goal at this stage is to get translations included in the Transifex pull request.
|
||||
|
||||
To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.
|
||||
To download translated strings automatically, you'll need to:
|
||||
|
||||

|
||||
1. Install the [Transifex CLI client](https://github.com/transifex/cli)
|
||||
2. Generate a [Transifex API token](https://app.transifex.com/user/settings/api/)
|
||||
|
||||
Enter a threshold percentage of 1 (to ensure all translations are captured) and select the `develop` branch, then click **Sync**. This will initiate a pull request to GitHub to update any newly modified translation (`.po`) files.
|
||||
Once you have the client set up, run the following command:
|
||||
|
||||
!!! tip
|
||||
The new PR should appear within a few minutes. If it does not, check that there are in fact new translations to be added.
|
||||
```no-highlight
|
||||
TX_TOKEN=$TOKEN tx pull
|
||||
```
|
||||
|
||||

|
||||
This will download all portable (`.po`) translation files from Transifex, updating them locally as needed.
|
||||
|
||||
Once the PR has been merged, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Update the `develop` branch locally to pull in the changes from the Transifex PR, then run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command:
|
||||
Once retrieved, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command to compile them:
|
||||
|
||||
```nohighlight
|
||||
```no-highlight
|
||||
./manage.py compilemessages
|
||||
```
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ img {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.md-content img {
|
||||
background-color: rgba(255, 255, 255, 0.64);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -41,7 +41,7 @@ NetBox integrates with the open source [python-social-auth](https://github.com/p
|
||||
* Google
|
||||
* Hashicorp Vault
|
||||
* Keycloak
|
||||
* Microsoft Azure AD
|
||||
* Microsoft Entra ID
|
||||
* Microsoft Graph
|
||||
* Okta
|
||||
* OIDC
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{style="height: 100px; margin-bottom: 3em"}
|
||||
{style="height: 100px; margin-bottom: 3em; background: none;"}
|
||||
{style="height: 100px; margin-bottom: 3em; background: none;"}
|
||||
|
||||
# The Premier Network Source of Truth
|
||||
|
||||
|
||||
BIN
docs/media/authentication/google_login_portal.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/media/authentication/netbox_google_login.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,6 +1,6 @@
|
||||
# IKE Policies
|
||||
|
||||
An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
|
||||
An [Internet Key Exchange (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
|
||||
|
||||
## Fields
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ The technology employed in forming and operating the L2VPN. Choices include:
|
||||
* VXLAN-EVPN
|
||||
* MPLS-EVPN
|
||||
* PBB-EVPN
|
||||
* EVPN-VPWS
|
||||
|
||||
!!! note
|
||||
Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations.
|
||||
|
||||
24
docs/netbox_logo_dark.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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: #00f2d4;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -10,6 +10,15 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.1](./version-4.1.md) (September 2024)
|
||||
|
||||
* Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))
|
||||
* VLAN Group ID Ranges ([#9627](https://github.com/netbox-community/netbox/issues/9627))
|
||||
* Nested Device Modules ([#10500](https://github.com/netbox-community/netbox/issues/10500))
|
||||
* Rack Types ([#12826](https://github.com/netbox-community/netbox/issues/12826))
|
||||
* Plugins Catalog Integration ([#14731](https://github.com/netbox-community/netbox/issues/14731))
|
||||
* User Notifications ([#15621](https://github.com/netbox-community/netbox/issues/15621))
|
||||
|
||||
#### [Version 4.0](./version-4.0.md) (April 2024)
|
||||
|
||||
* Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
|
||||
|
||||
@@ -1,5 +1,142 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.10 (2024-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18260](https://github.com/netbox-community/netbox/issues/18260) - Fix object change logging
|
||||
|
||||
---
|
||||
|
||||
## v4.1.9 (2024-12-17)
|
||||
|
||||
!!! danger "Do Not Use"
|
||||
This release contains a regression which breaks change logging. Please use release v4.1.10 instead.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17215](https://github.com/netbox-community/netbox/issues/17215) - Change the highlighted color of disabled interfaces in interface lists
|
||||
* [#18224](https://github.com/netbox-community/netbox/issues/18224) - Apply all registered request processors when running custom scripts
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16757](https://github.com/netbox-community/netbox/issues/16757) - Fix rendering of IP addresses table when assigning an existing IP address to an interface with global HTMX navigation enabled
|
||||
* [#17868](https://github.com/netbox-community/netbox/issues/17868) - Fix `ZeroDivisionError` exception under specific circumstances when generating a cable trace
|
||||
* [#18124](https://github.com/netbox-community/netbox/issues/18124) - Enable referencing cable attributes when querying a `cabletermination_set` via the GraphQL API
|
||||
* [#18230](https://github.com/netbox-community/netbox/issues/18230) - Fix `AttributeError` exception when attempting to edit an IP address assigned to a virtual machine interface
|
||||
|
||||
---
|
||||
|
||||
## v4.1.8 (2024-12-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17071](https://github.com/netbox-community/netbox/issues/17071) - Enable OOB IP address designation during bulk import
|
||||
* [#17465](https://github.com/netbox-community/netbox/issues/17465) - Enable designation of rack type during bulk import & bulk edit
|
||||
* [#17889](https://github.com/netbox-community/netbox/issues/17889) - Enable designating an IP address as out-of-band for a device upon creation
|
||||
* [#17960](https://github.com/netbox-community/netbox/issues/17960) - Add L2TP, PPTP, Wireguard, and OpenVPN tunnel types
|
||||
* [#18021](https://github.com/netbox-community/netbox/issues/18021) - Automatically clear cache on restart when `DEBUG` is enabled
|
||||
* [#18061](https://github.com/netbox-community/netbox/issues/18061) - Omit stack trace from rendered device/VM configuration when an exception is raised
|
||||
* [#18065](https://github.com/netbox-community/netbox/issues/18065) - Include status in device details when hovering on rack elevation
|
||||
* [#18211](https://github.com/netbox-community/netbox/issues/18211) - Enable the dynamic registration of context managers for request processing
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14044](https://github.com/netbox-community/netbox/issues/14044) - Fix unhandled AttributeError exception when bulk renaming objects
|
||||
* [#17490](https://github.com/netbox-community/netbox/issues/17490) - Fix dynamic inclusion support for config templates
|
||||
* [#17810](https://github.com/netbox-community/netbox/issues/17810) - Fix validation of racked device fields when modifying via REST API
|
||||
* [#17820](https://github.com/netbox-community/netbox/issues/17820) - Ensure default custom field values are populated when creating new modules
|
||||
* [#18044](https://github.com/netbox-community/netbox/issues/18044) - Show plugin-generated alerts within UI views for custom scripts
|
||||
* [#18150](https://github.com/netbox-community/netbox/issues/18150) - Fix REST API pagination for low `MAX_PAGE_SIZE` values
|
||||
* [#18183](https://github.com/netbox-community/netbox/issues/18183) - Omit UI navigation bar when printing
|
||||
* [#18213](https://github.com/netbox-community/netbox/issues/18213) - Fix searching for ASN ranges by name
|
||||
|
||||
---
|
||||
|
||||
## v4.1.7 (2024-11-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#15239](https://github.com/netbox-community/netbox/issues/15239) - Enable adding/removing individual VLANs while bulk editing device interfaces
|
||||
* [#17871](https://github.com/netbox-community/netbox/issues/17871) - Enable the assignment/removal of virtualization cluster via device bulk edit
|
||||
* [#17934](https://github.com/netbox-community/netbox/issues/17934) - Add 1000Base-LX interface type
|
||||
* [#18007](https://github.com/netbox-community/netbox/issues/18007) - Hide sensitive parameters under data source view (even for privileged users)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17459](https://github.com/netbox-community/netbox/issues/17459) - Correct help text on `name` field of module type component templates
|
||||
* [#17901](https://github.com/netbox-community/netbox/issues/17901) - Ensure GraphiQL UI resources are served locally
|
||||
* [#17921](https://github.com/netbox-community/netbox/issues/17921) - Fix scheduling of recurring custom scripts
|
||||
* [#17923](https://github.com/netbox-community/netbox/issues/17923) - Fix the execution of custom scripts via REST API & management command
|
||||
* [#17963](https://github.com/netbox-community/netbox/issues/17963) - Fix selection of all listed objects during bulk edit
|
||||
* [#17969](https://github.com/netbox-community/netbox/issues/17969) - Fix system info export when a config revision exists
|
||||
* [#17972](https://github.com/netbox-community/netbox/issues/17972) - Force evaluation of `LOGIN_REQUIRED` when requesting static media
|
||||
* [#17986](https://github.com/netbox-community/netbox/issues/17986) - Correct labels for virtual machine & virtual disk size properties
|
||||
* [#18037](https://github.com/netbox-community/netbox/issues/18037) - Fix validation of maximum VLAN ID value when defining VLAN groups
|
||||
* [#18038](https://github.com/netbox-community/netbox/issues/18038) - The `to_grams()` utility function should always return an integer value
|
||||
|
||||
---
|
||||
|
||||
## v4.1.6 (2024-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17700](https://github.com/netbox-community/netbox/issues/17700) - Fix warning when no scripts are found within a script module
|
||||
* [#17884](https://github.com/netbox-community/netbox/issues/17884) - Fix translation support for certain tab headings
|
||||
* [#17885](https://github.com/netbox-community/netbox/issues/17885) - Fix regression preventing custom scripts from executing
|
||||
|
||||
## v4.1.5 (2024-10-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#17789](https://github.com/netbox-community/netbox/issues/17789) - Provide a single "scope" field for bulk editing VLAN group scope assignments
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17358](https://github.com/netbox-community/netbox/issues/17358) - Fix validation of overlapping IP ranges
|
||||
* [#17374](https://github.com/netbox-community/netbox/issues/17374) - Fix styling of highlighted table rows in dark mode
|
||||
* [#17460](https://github.com/netbox-community/netbox/issues/17460) - Ensure bulk action buttons are consistent for device type components
|
||||
* [#17635](https://github.com/netbox-community/netbox/issues/17635) - Ensure AbortTransaction is caught when running a custom script with `commit=False`
|
||||
* [#17685](https://github.com/netbox-community/netbox/issues/17685) - Ensure background jobs are validated before being scheduled
|
||||
* [#17710](https://github.com/netbox-community/netbox/issues/17710) - Remove cached fields on CableTermination model from GraphQL API
|
||||
* [#17740](https://github.com/netbox-community/netbox/issues/17740) - Ensure support for image attachments with a `.webp` file extension
|
||||
* [#17749](https://github.com/netbox-community/netbox/issues/17749) - Restore missing `devicetypes` and `children` fields for several objects in GraphQL API
|
||||
* [#17754](https://github.com/netbox-community/netbox/issues/17754) - Remove paginator from version history table under plugin view
|
||||
* [#17759](https://github.com/netbox-community/netbox/issues/17759) - Retain `job_timeout` value when scheduling a recurring custom script
|
||||
* [#17774](https://github.com/netbox-community/netbox/issues/17774) - Fix SSO login support for Entra ID (formerly Azure AD)
|
||||
* [#17802](https://github.com/netbox-community/netbox/issues/17802) - Fix background color for bulk rename buttons in list views
|
||||
* [#17838](https://github.com/netbox-community/netbox/issues/17838) - Adjust `manage.py` to reference `python3` executable
|
||||
|
||||
---
|
||||
|
||||
## v4.1.4 (2024-10-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11671](https://github.com/netbox-community/netbox/issues/11671) - Display device's rack position in cable traces
|
||||
* [#15829](https://github.com/netbox-community/netbox/issues/15829) - Rename Microsoft Azure AD SSO backend to Microsoft Entra ID
|
||||
* [#16009](https://github.com/netbox-community/netbox/issues/16009) - Float form & bulk operation buttons within UI
|
||||
* [#17079](https://github.com/netbox-community/netbox/issues/17079) - Introduce additional choices for device airflow direction
|
||||
* [#17216](https://github.com/netbox-community/netbox/issues/17216) - Add EVPN-VPWS L2VPN type
|
||||
* [#17655](https://github.com/netbox-community/netbox/issues/17655) - Limit the display of tagged VLANs within interface tables
|
||||
* [#17669](https://github.com/netbox-community/netbox/issues/17669) - Enable filtering VLANs by assigned device or VM interface
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16024](https://github.com/netbox-community/netbox/issues/16024) - Fix AND/OR filtering in GraphQL API for selection fields
|
||||
* [#17400](https://github.com/netbox-community/netbox/issues/17400) - Fix cable tracing across split paths
|
||||
* [#17562](https://github.com/netbox-community/netbox/issues/17562) - Fix GraphQL API query support for custom field choices
|
||||
* [#17566](https://github.com/netbox-community/netbox/issues/17566) - Fix AttributeError exception resulting from background jobs with no associated object type
|
||||
* [#17614](https://github.com/netbox-community/netbox/issues/17614) - Disallow removal of a master device from its virtual chassis
|
||||
* [#17636](https://github.com/netbox-community/netbox/issues/17636) - Fix filtering of related objects when adding a power port, rear port, or inventory item template to a device type
|
||||
* [#17644](https://github.com/netbox-community/netbox/issues/17644) - Correct sizing of logo & SSO icons on login page
|
||||
* [#17648](https://github.com/netbox-community/netbox/issues/17648) - Fix AttributeError exception when attempting to delete a background job under certain conditions
|
||||
* [#17663](https://github.com/netbox-community/netbox/issues/17663) - Fix extended lookups for choice field filters
|
||||
* [#17671](https://github.com/netbox-community/netbox/issues/17671) - Fix the display of rack types in global search results
|
||||
* [#17713](https://github.com/netbox-community/netbox/issues/17713) - Fix UnboundLocalError exception when attempting to sync data source in parallel
|
||||
|
||||
---
|
||||
|
||||
## v4.1.3 (2024-10-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -156,7 +156,8 @@ nav:
|
||||
- Administration:
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
- Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
|
||||
- Google: 'administration/authentication/google.md'
|
||||
- Microsoft Entra ID: 'administration/authentication/microsoft-entra-id.md'
|
||||
- Okta: 'administration/authentication/okta.md'
|
||||
- Permissions: 'administration/permissions.md'
|
||||
- Error Reporting: 'administration/error-reporting.md'
|
||||
|
||||
@@ -18,7 +18,7 @@ __all__ = [
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class CircuitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import signals, search
|
||||
from . import signals, search # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -171,7 +171,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
f"id,cid,description,status",
|
||||
"id,cid,description,status",
|
||||
f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
|
||||
@@ -16,7 +16,7 @@ __all__ = (
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import Job
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.migrations.operations import AlterModelOptions
|
||||
|
||||
@@ -16,9 +18,14 @@ class CoreConfig(AppConfig):
|
||||
name = "core"
|
||||
|
||||
def ready(self):
|
||||
from core.api import schema # noqa
|
||||
from core.api import schema # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, events, search
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
from netbox import context_managers # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Clear Redis cache on startup in development mode
|
||||
if settings.DEBUG:
|
||||
cache.clear()
|
||||
|
||||
@@ -34,7 +34,7 @@ class LocalBackend(DataBackend):
|
||||
|
||||
@contextmanager
|
||||
def fetch(self):
|
||||
logger.debug(f"Data source type is local; skipping fetch")
|
||||
logger.debug("Data source type is local; skipping fetch")
|
||||
local_path = urlparse(self.url).path # Strip file:// scheme
|
||||
|
||||
yield local_path
|
||||
|
||||
@@ -15,7 +15,7 @@ __all__ = (
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: # noqa: F821
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
|
||||
@@ -26,7 +26,7 @@ class Command(BaseCommand):
|
||||
if invalid_names := set(options['name']) - found_names:
|
||||
raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}")
|
||||
else:
|
||||
raise CommandError(f"Must specify at least one data source, or set --all.")
|
||||
raise CommandError("Must specify at least one data source, or set --all.")
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Syncing {len(datasources)} data sources.")
|
||||
@@ -43,4 +43,4 @@ class Command(BaseCommand):
|
||||
raise e
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Finished.")
|
||||
self.stdout.write("Finished.")
|
||||
|
||||
@@ -125,7 +125,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
# Ensure URL scheme matches selected type
|
||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||
raise ValidationError({
|
||||
'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
|
||||
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||
})
|
||||
|
||||
def to_objectchange(self, action):
|
||||
@@ -201,7 +201,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
logger.debug(f"Updated {updated_count} files")
|
||||
|
||||
# Bulk delete deleted files
|
||||
deleted_count, _ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
deleted_count, __ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
logger.debug(f"Deleted {deleted_count} files")
|
||||
|
||||
# Walk the local replication to find new files
|
||||
|
||||
@@ -9,12 +9,11 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from rq.exceptions import InvalidJobOperation
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.rqworker import get_queue_for_model
|
||||
|
||||
@@ -118,10 +117,11 @@ class Job(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
# TODO: Employ dynamic registration
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type.model == 'scriptmodule':
|
||||
return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type:
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
|
||||
elif self.object_type.model == 'scriptmodule':
|
||||
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
|
||||
return reverse('core:job', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
@@ -131,7 +131,7 @@ class Job(models.Model):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.object_type not in ObjectType.objects.with_feature('jobs'):
|
||||
if self.object_type and self.object_type not in ObjectType.objects.with_feature('jobs'):
|
||||
raise ValidationError(
|
||||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
@@ -154,12 +154,16 @@ class Job(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT)
|
||||
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
job = queue.fetch_job(str(self.job_id))
|
||||
|
||||
if job:
|
||||
job.cancel()
|
||||
try:
|
||||
job.cancel()
|
||||
except InvalidJobOperation:
|
||||
# Job may raise this exception from get_status() if missing from Redis
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -224,7 +228,7 @@ class Job(models.Model):
|
||||
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||
job = Job.objects.create(
|
||||
job = Job(
|
||||
object_type=object_type,
|
||||
object_id=object_id,
|
||||
name=name,
|
||||
@@ -234,6 +238,8 @@ class Job(models.Model):
|
||||
user=user,
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
job.full_clean()
|
||||
job.save()
|
||||
|
||||
# Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous
|
||||
# (blocking) operation, and execution will pause until the job completes.
|
||||
|
||||
@@ -308,6 +308,7 @@ class BackgroundTaskTestCase(TestCase):
|
||||
worker = get_worker('default')
|
||||
job = queue.enqueue(self.dummy_job_default)
|
||||
worker.prepare_job_execution(job)
|
||||
worker.prepare_execution(job)
|
||||
|
||||
self.assertEqual(job.get_status(), JobStatus.STARTED)
|
||||
|
||||
@@ -345,3 +346,32 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertIn(str(worker1.name), str(response.content))
|
||||
self.assertIn('Birth', str(response.content))
|
||||
self.assertIn('Total working time', str(response.content))
|
||||
|
||||
|
||||
class SystemTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
def test_system_view_default(self):
|
||||
# Test UI render
|
||||
response = self.client.get(reverse('core:system'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test export
|
||||
response = self.client.get(f"{reverse('core:system')}?export=true")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_system_view_with_config_revision(self):
|
||||
ConfigRevision.objects.create()
|
||||
|
||||
# Test UI render
|
||||
response = self.client.get(reverse('core:system'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test export
|
||||
response = self.client.get(f"{reverse('core:system')}?export=true")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -626,11 +626,7 @@ class SystemView(UserPassesTestMixin, View):
|
||||
}
|
||||
|
||||
# Configuration
|
||||
try:
|
||||
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
|
||||
except ConfigRevision.DoesNotExist:
|
||||
# Fall back to using the active config data if no record is found
|
||||
config = get_config()
|
||||
config = get_config()
|
||||
|
||||
# Raw data export
|
||||
if 'export' in request.GET:
|
||||
|
||||
@@ -56,7 +56,7 @@ __all__ = [
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Platform
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import DeviceRole, InventoryItemRole
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
|
||||
@@ -10,7 +10,7 @@ class DCIMConfig(AppConfig):
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from utilities.counters import connect_counters
|
||||
from . import signals, search
|
||||
from . import signals, search # noqa: F401
|
||||
from .models import CableTermination, Device, DeviceType, VirtualChassis
|
||||
|
||||
# Register models
|
||||
|
||||
@@ -197,6 +197,9 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
|
||||
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
|
||||
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
|
||||
AIRFLOW_REAR_TO_SIDE = 'rear-to-side'
|
||||
AIRFLOW_BOTTOM_TO_TOP = 'bottom-to-top'
|
||||
AIRFLOW_TOP_TO_BOTTOM = 'top-to-bottom'
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
AIRFLOW_MIXED = 'mixed'
|
||||
|
||||
@@ -206,6 +209,9 @@ class DeviceAirflowChoices(ChoiceSet):
|
||||
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
|
||||
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
|
||||
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
|
||||
(AIRFLOW_REAR_TO_SIDE, _('Rear to side')),
|
||||
(AIRFLOW_BOTTOM_TO_TOP, _('Bottom to top')),
|
||||
(AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')),
|
||||
(AIRFLOW_PASSIVE, _('Passive')),
|
||||
(AIRFLOW_MIXED, _('Mixed')),
|
||||
)
|
||||
@@ -865,6 +871,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_100ME_T1 = '100base-t1'
|
||||
TYPE_100ME_SFP = '100base-x-sfp'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_LX_FIXED = '1000base-lx'
|
||||
TYPE_1GE_TX_FIXED = '1000base-tx'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
TYPE_1GE_SFP = '1000base-x-sfp'
|
||||
@@ -1027,6 +1034,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
|
||||
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
|
||||
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
|
||||
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
|
||||
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
|
||||
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
|
||||
|
||||
@@ -271,7 +271,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('id', 'name', 'slug', 'status', 'facility', 'description')
|
||||
fields = ('id', 'name', 'slug', 'facility', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -13,10 +13,11 @@ from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from virtualization.models import Cluster
|
||||
from wireless.choices import WirelessRoleChoices
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
|
||||
__all__ = (
|
||||
'CableBulkEditForm',
|
||||
@@ -358,6 +359,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False
|
||||
)
|
||||
rack_type = DynamicModelChoiceField(
|
||||
label=_('Rack type'),
|
||||
queryset=RackType.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
@@ -437,7 +443,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
|
||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
@@ -721,6 +727,14 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
label=_('Cluster'),
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': ['$site', 'null']
|
||||
},
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = Device
|
||||
@@ -729,9 +743,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('site', 'location', name=_('Location')),
|
||||
FieldSet('manufacturer', 'device_type', 'airflow', 'serial', name=_('Hardware')),
|
||||
FieldSet('config_template', name=_('Configuration')),
|
||||
FieldSet('cluster', name=_('Virtualization')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
|
||||
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'cluster', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@@ -1404,18 +1419,25 @@ class InterfaceBulkEditForm(
|
||||
parent = DynamicModelChoiceField(
|
||||
label=_('Parent'),
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_chassis_member_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
label=_('Bridge'),
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_chassis_member_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'type': 'lag',
|
||||
'virtual_chassis_member_id': '$device',
|
||||
},
|
||||
label=_('LAG')
|
||||
)
|
||||
@@ -1472,6 +1494,7 @@ class InterfaceBulkEditForm(
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
},
|
||||
label=_('Untagged VLAN')
|
||||
)
|
||||
@@ -1480,9 +1503,28 @@ class InterfaceBulkEditForm(
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
},
|
||||
label=_('Tagged VLANs')
|
||||
)
|
||||
add_tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
label=_('Add tagged VLANs'),
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
},
|
||||
)
|
||||
remove_tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
label=_('Remove tagged VLANs'),
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
}
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -1509,7 +1551,13 @@ class InterfaceBulkEditForm(
|
||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', name=_('802.1Q Switching')),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('tagged_vlans', name=_('Assignment')),
|
||||
FieldSet('add_tagged_vlans', 'remove_tagged_vlans', name=_('Add/Remove')),
|
||||
),
|
||||
),
|
||||
FieldSet(
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
||||
name=_('Wireless')
|
||||
@@ -1523,19 +1571,7 @@ class InterfaceBulkEditForm(
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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)
|
||||
self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
||||
else:
|
||||
if not self.device_id:
|
||||
# See #4523
|
||||
if 'pk' in self.initial:
|
||||
site = None
|
||||
@@ -1559,6 +1595,13 @@ class InterfaceBulkEditForm(
|
||||
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||
)
|
||||
|
||||
self.fields['add_tagged_vlans'].widget.add_query_param(
|
||||
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||
)
|
||||
self.fields['remove_tagged_vlans'].widget.add_query_param(
|
||||
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||
)
|
||||
|
||||
self.fields['parent'].choices = ()
|
||||
self.fields['parent'].widget.attrs['disabled'] = True
|
||||
self.fields['bridge'].choices = ()
|
||||
|
||||
@@ -256,6 +256,13 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Name of assigned role')
|
||||
)
|
||||
rack_type = CSVModelChoiceField(
|
||||
label=_('Rack type'),
|
||||
queryset=RackType.objects.all(),
|
||||
to_field_name='model',
|
||||
required=False,
|
||||
help_text=_('Rack type model')
|
||||
)
|
||||
form_factor = CSVChoiceField(
|
||||
label=_('Type'),
|
||||
choices=RackFormFactorChoices,
|
||||
@@ -265,8 +272,13 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
width = forms.ChoiceField(
|
||||
label=_('Width'),
|
||||
choices=RackWidthChoices,
|
||||
required=False,
|
||||
help_text=_('Rail-to-rail width (in inches)')
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Height (U)')
|
||||
)
|
||||
outer_unit = CSVChoiceField(
|
||||
label=_('Outer unit'),
|
||||
choices=RackDimensionUnitChoices,
|
||||
@@ -289,9 +301,9 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow',
|
||||
'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
|
||||
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@@ -303,6 +315,16 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# width & u_height must be set if not specifying a rack type on import
|
||||
if not self.instance.pk:
|
||||
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('width'):
|
||||
raise forms.ValidationError(_("Width must be set if not specifying a rack type."))
|
||||
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('u_height'):
|
||||
raise forms.ValidationError(_("U height must be set if not specifying a rack type."))
|
||||
|
||||
|
||||
class RackReservationImportForm(NetBoxModelImportForm):
|
||||
site = CSVModelChoiceField(
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.models import *
|
||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
from .model_forms import CableForm
|
||||
|
||||
|
||||
|
||||
@@ -909,6 +909,13 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
if self.instance.pk:
|
||||
self.fields['module_type'].disabled = True
|
||||
|
||||
# Components attached to a module need to present this standardized substitution help text.
|
||||
self.fields['name'].help_text = _(
|
||||
"Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
|
||||
"supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
|
||||
"automatically replaced with the position value when creating a new module."
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
@@ -954,7 +961,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'device_type_id': '$device_type',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1001,8 +1008,8 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
'device_type_id': '$device_type',
|
||||
'module_type_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1063,7 +1070,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
|
||||
queryset=InventoryItemTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type'
|
||||
'device_type_id': '$device_type'
|
||||
}
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
|
||||
@@ -243,14 +243,6 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
|
||||
class Meta(model_forms.InterfaceForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'module' in self.fields:
|
||||
self.fields['name'].help_text += _(
|
||||
"The string <code>{module}</code> will be replaced with the position of the assigned module, if any."
|
||||
)
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
device = DynamicModelChoiceField(
|
||||
@@ -261,8 +253,8 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
# TODO: Clean up the application of HTMXSelect attributes
|
||||
attrs={
|
||||
'hx-get': '.',
|
||||
'hx-include': f'#form_fields',
|
||||
'hx-target': f'#form_fields',
|
||||
'hx-include': '#form_fields',
|
||||
'hx-target': '#form_fields',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
__all__ = (
|
||||
'CabledObjectMixin',
|
||||
@@ -11,18 +10,18 @@ __all__ = (
|
||||
|
||||
@strawberry.type
|
||||
class CabledObjectMixin:
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None # noqa: F821
|
||||
|
||||
link_peers: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
], strawberry.union("LinkPeerType")]]
|
||||
|
||||
|
||||
@@ -30,14 +29,14 @@ class CabledObjectMixin:
|
||||
class PathEndpointMixin:
|
||||
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
], strawberry.union("ConnectedEndpointType")]]
|
||||
|
||||
@@ -112,11 +112,11 @@ class ModularComponentTemplateType(ComponentTemplateType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CableTermination,
|
||||
exclude=('termination_type', 'termination_id'),
|
||||
exclude=('termination_type', 'termination_id', '_device', '_rack', '_location', '_site'),
|
||||
filters=CableTerminationFilter
|
||||
)
|
||||
class CableTerminationType(NetBoxObjectType):
|
||||
|
||||
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
termination: Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
|
||||
@@ -243,6 +243,7 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo
|
||||
consoleserverports: List[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]
|
||||
poweroutlets: List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]
|
||||
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
|
||||
devicebays: List[Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]]
|
||||
modulebays: List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]
|
||||
services: List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]
|
||||
inventoryitems: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
@@ -60,7 +60,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths')))
|
||||
|
||||
# Reinitialize the model's PK sequence
|
||||
self.stdout.write(f'Resetting database sequence for CablePath model')
|
||||
self.stdout.write('Resetting database sequence for CablePath model')
|
||||
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath])
|
||||
with connection.cursor() as cursor:
|
||||
for sql in sequence_sql:
|
||||
|
||||
@@ -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._state.adding and (not self.a_terminations or not self.b_terminations):
|
||||
if self._state.adding and self.pk is None 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:
|
||||
@@ -666,6 +666,14 @@ class CablePath(models.Model):
|
||||
rear_port_id=remote_terminations[0].pk,
|
||||
rear_port_position__in=position_stack.pop()
|
||||
)
|
||||
# If all rear ports have a single position, we can just get the front ports
|
||||
elif all([rp.positions == 1 for rp in remote_terminations]):
|
||||
front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
|
||||
|
||||
if len(front_ports) != len(remote_terminations):
|
||||
# Some rear ports does not have a front port
|
||||
is_split = True
|
||||
break
|
||||
else:
|
||||
# No position indicated: path has split, so we stop at the RearPorts
|
||||
is_split = True
|
||||
|
||||
@@ -160,7 +160,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
|
||||
def _get_module_tree(self, module):
|
||||
modules = []
|
||||
all_module_bays = module.device.modulebays.all().select_related('module')
|
||||
while module:
|
||||
modules.append(module)
|
||||
if module.module_bay:
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
@@ -22,7 +22,6 @@ from utilities.tracking import TrackingModelMixin
|
||||
from wireless.choices import *
|
||||
from wireless.utils import get_channel_attr
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'CabledObjectModel',
|
||||
|
||||
@@ -983,6 +983,13 @@ class Device(
|
||||
'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
|
||||
})
|
||||
|
||||
if hasattr(self, 'vc_master_for') and self.vc_master_for and self.vc_master_for != self.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'virtual_chassis': _('Device cannot be removed from virtual chassis {virtual_chassis} because it is currently designated as its master.').format(
|
||||
virtual_chassis=self.vc_master_for
|
||||
)
|
||||
})
|
||||
|
||||
def _instantiate_components(self, queryset, bulk_create=True):
|
||||
"""
|
||||
Instantiate components for the device from the specified component templates.
|
||||
@@ -1270,6 +1277,11 @@ class Module(PrimaryModel, ConfigContextModel):
|
||||
if not disable_replication:
|
||||
create_instances.append(template_instance)
|
||||
|
||||
# Set default values for any applicable custom fields
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(component_model):
|
||||
for component in create_instances:
|
||||
component.custom_field_data = cf_defaults
|
||||
|
||||
if component_model is not ModuleBay:
|
||||
component_model.objects.bulk_create(create_instances)
|
||||
# Emit the post_save signal for each newly created object
|
||||
|
||||
@@ -250,7 +250,7 @@ class RackTypeIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('type', 'description')
|
||||
display_attrs = ('model', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -162,6 +162,9 @@ class CableTraceSVG:
|
||||
location_label += f' / {instance.location}'
|
||||
if instance.rack:
|
||||
location_label += f' / {instance.rack}'
|
||||
if instance.position:
|
||||
location_label += f' / {instance.get_face_display()}'
|
||||
location_label += f' / U{instance.position}'
|
||||
labels.append(location_label)
|
||||
elif instance._meta.model_name == 'circuit':
|
||||
labels[0] = f'Circuit {instance}'
|
||||
@@ -359,7 +362,7 @@ class CableTraceSVG:
|
||||
self.cursor += CABLE_HEIGHT
|
||||
|
||||
# Connector (a Cable or WirelessLink)
|
||||
if links:
|
||||
if links and far_ends:
|
||||
|
||||
obj_list = {end.parent_object for end in far_ends}
|
||||
parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
|
||||
|
||||
@@ -48,6 +48,7 @@ def get_device_description(device):
|
||||
|
||||
Name: <name>
|
||||
Role: <role>
|
||||
Status: <status>
|
||||
Device Type: <manufacturer> <model> (<u_height>)
|
||||
Asset tag: <asset_tag> (if defined)
|
||||
Serial: <serial> (if defined)
|
||||
@@ -55,6 +56,7 @@ def get_device_description(device):
|
||||
"""
|
||||
description = f'Name: {device.name}'
|
||||
description += f'\nRole: {device.role}'
|
||||
description += f'\nStatus: {device.get_status_display()}'
|
||||
u_height = f'{floatformat(device.device_type.u_height)}U'
|
||||
description += f'\nDevice Type: {device.device_type.manufacturer.name} {device.device_type.model} ({u_height})'
|
||||
if device.asset_tag:
|
||||
|
||||
@@ -588,6 +588,9 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
def value_ip_addresses(self, value):
|
||||
return ",".join([str(obj.address) for obj in value.all()])
|
||||
|
||||
def value_tagged_vlans(self, value):
|
||||
return ",".join([str(obj) for obj in value.all()])
|
||||
|
||||
|
||||
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim import models
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
@@ -56,9 +56,13 @@ INTERFACE_FHRPGROUPS = """
|
||||
|
||||
INTERFACE_TAGGED_VLANS = """
|
||||
{% if record.mode == 'tagged' %}
|
||||
{% if value.count > 3 %}
|
||||
<a href="{% url 'ipam:vlan_list' %}?{{ record|meta:"model_name" }}_id={{ record.pk }}">{{ value.count }} VLANs</a>
|
||||
{% else %}
|
||||
{% for vlan in value.all %}
|
||||
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
All
|
||||
{% endif %}
|
||||
|
||||
@@ -2135,12 +2135,12 @@ class ConnectedDeviceTest(APITestCase):
|
||||
def test_get_connected_device(self):
|
||||
url = reverse('dcim-api:connected-device-list')
|
||||
|
||||
url_params = f'?peer_device=TestDevice1&peer_interface=eth0'
|
||||
url_params = '?peer_device=TestDevice1&peer_interface=eth0'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['name'], 'TestDevice2')
|
||||
|
||||
url_params = f'?peer_device=TestDevice1&peer_interface=eth1'
|
||||
url_params = '?peer_device=TestDevice1&peer_interface=eth1'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@@ -2060,6 +2060,49 @@ class CablePathTestCase(TestCase):
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interface1).render()
|
||||
|
||||
def test_222_single_path_via_multiple_singleposition_rear_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
[FP2] [RP2]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[frontport1, frontport2]
|
||||
)
|
||||
cable1.save()
|
||||
self.assertEqual(CablePath.objects.count(), 1)
|
||||
|
||||
cable2 = Cable(
|
||||
a_terminations=[rearport1, rearport2],
|
||||
b_terminations=[interface2]
|
||||
)
|
||||
cable2.save()
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
self.assertPathExists(
|
||||
(interface1, cable1, (frontport1, frontport2), (rearport1, rearport2), cable2, interface2),
|
||||
is_complete=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interface2, cable2, (rearport1, rearport2), (frontport1, frontport2), cable1, interface1),
|
||||
is_complete=True
|
||||
)
|
||||
|
||||
# Test SVG generation both directions
|
||||
CableTraceSVG(interface1).render()
|
||||
CableTraceSVG(interface2).render()
|
||||
|
||||
def test_301_create_path_via_existing_cable(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
||||
@@ -4838,13 +4838,6 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'device_role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_role(self):
|
||||
role = DeviceRole.objects.all()[:2]
|
||||
params = {'role_id': [role[0].pk, role[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
|
||||
@@ -662,10 +662,8 @@ class ModuleBayTestCase(TestCase):
|
||||
|
||||
def test_module_bay_recursion(self):
|
||||
module_bay_1 = ModuleBay.objects.get(name='Module Bay 1')
|
||||
module_bay_2 = ModuleBay.objects.get(name='Module Bay 2')
|
||||
module_bay_3 = ModuleBay.objects.get(name='Module Bay 3')
|
||||
module_1 = Module.objects.get(module_bay=module_bay_1)
|
||||
module_2 = Module.objects.get(module_bay=module_bay_2)
|
||||
module_3 = Module.objects.get(module_bay=module_bay_3)
|
||||
|
||||
# Confirm error if ModuleBay recurses
|
||||
@@ -681,8 +679,6 @@ class ModuleBayTestCase(TestCase):
|
||||
module_1.save()
|
||||
|
||||
def test_single_module_token(self):
|
||||
module_bays = ModuleBay.objects.all()
|
||||
modules = Module.objects.all()
|
||||
device_type = DeviceType.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
site = Site.objects.first()
|
||||
@@ -708,7 +704,7 @@ class ModuleBayTestCase(TestCase):
|
||||
location=location,
|
||||
rack=rack
|
||||
)
|
||||
cp = device.consoleports.first()
|
||||
device.consoleports.first()
|
||||
|
||||
def test_nested_module_token(self):
|
||||
pass
|
||||
@@ -733,39 +729,41 @@ class CableTestCase(TestCase):
|
||||
device2 = Device.objects.create(
|
||||
device_type=devicetype, role=role, name='TestDevice2', site=site
|
||||
)
|
||||
interface1 = Interface.objects.create(device=device1, name='eth0')
|
||||
interface2 = Interface.objects.create(device=device2, name='eth0')
|
||||
interface3 = Interface.objects.create(device=device2, name='eth1')
|
||||
Cable(a_terminations=[interface1], b_terminations=[interface2]).save()
|
||||
interfaces = (
|
||||
Interface(device=device1, name='eth0'),
|
||||
Interface(device=device2, name='eth0'),
|
||||
Interface(device=device2, name='eth1'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]]).save()
|
||||
PowerPort.objects.create(device=device2, name='psu1')
|
||||
|
||||
power_port1 = PowerPort.objects.create(device=device2, name='psu1')
|
||||
patch_pannel = Device.objects.create(
|
||||
patch_panel = Device.objects.create(
|
||||
device_type=devicetype, role=role, name='TestPatchPanel', site=site
|
||||
)
|
||||
rear_port1 = RearPort.objects.create(device=patch_pannel, name='RP1', type='8p8c')
|
||||
front_port1 = FrontPort.objects.create(
|
||||
device=patch_pannel, name='FP1', type='8p8c', rear_port=rear_port1, rear_port_position=1
|
||||
rear_ports = (
|
||||
RearPort(device=patch_panel, name='RP1', type='8p8c'),
|
||||
RearPort(device=patch_panel, name='RP2', type='8p8c', positions=2),
|
||||
RearPort(device=patch_panel, name='RP3', type='8p8c', positions=3),
|
||||
RearPort(device=patch_panel, name='RP4', type='8p8c', positions=3),
|
||||
)
|
||||
rear_port2 = RearPort.objects.create(device=patch_pannel, name='RP2', type='8p8c', positions=2)
|
||||
front_port2 = FrontPort.objects.create(
|
||||
device=patch_pannel, name='FP2', type='8p8c', rear_port=rear_port2, rear_port_position=1
|
||||
)
|
||||
rear_port3 = RearPort.objects.create(device=patch_pannel, name='RP3', type='8p8c', positions=3)
|
||||
front_port3 = FrontPort.objects.create(
|
||||
device=patch_pannel, name='FP3', type='8p8c', rear_port=rear_port3, rear_port_position=1
|
||||
)
|
||||
rear_port4 = RearPort.objects.create(device=patch_pannel, name='RP4', type='8p8c', positions=3)
|
||||
front_port4 = FrontPort.objects.create(
|
||||
device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
front_ports = (
|
||||
FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
|
||||
FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
|
||||
)
|
||||
FrontPort.objects.bulk_create(front_ports)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
|
||||
circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
|
||||
CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
|
||||
@@ -2571,7 +2571,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
f"device,name,type,vrf.pk,poe_mode,poe_type",
|
||||
"device,name,type,vrf.pk,poe_mode,poe_type",
|
||||
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import itertools
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import traceback
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
@@ -11,7 +9,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
@@ -35,7 +33,7 @@ from virtualization.forms import VirtualMachineFilterForm
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.tables import VirtualMachineTable
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
|
||||
CABLE_TERMINATION_TYPES = {
|
||||
@@ -2106,7 +2104,8 @@ class DeviceRenderConfigView(generic.ObjectView):
|
||||
# If a direct export has been requested, return the rendered template content as a
|
||||
# downloadable file.
|
||||
if request.GET.get('export'):
|
||||
response = HttpResponse(context['rendered_config'], content_type='text')
|
||||
content = context['rendered_config'] or context['error_message']
|
||||
response = HttpResponse(content, content_type='text')
|
||||
filename = f"{instance.name or 'config'}.txt"
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
@@ -2124,17 +2123,18 @@ class DeviceRenderConfigView(generic.ObjectView):
|
||||
|
||||
# Render the config template
|
||||
rendered_config = None
|
||||
error_message = None
|
||||
if config_template := instance.get_config_template():
|
||||
try:
|
||||
rendered_config = config_template.render(context=context_data)
|
||||
except TemplateError as e:
|
||||
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
|
||||
rendered_config = traceback.format_exc()
|
||||
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
|
||||
|
||||
return {
|
||||
'config_template': config_template,
|
||||
'context_data': context_data,
|
||||
'rendered_config': rendered_config,
|
||||
'error_message': error_message,
|
||||
}
|
||||
|
||||
|
||||
@@ -2616,6 +2616,16 @@ class InterfaceBulkEditView(generic.BulkEditView):
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
def post_save_operations(self, form, obj):
|
||||
super().post_save_operations(form, obj)
|
||||
|
||||
# Add/remove tagged VLANs
|
||||
if obj.mode == InterfaceModeChoices.MODE_TAGGED:
|
||||
if form.cleaned_data.get('add_tagged_vlans', None):
|
||||
obj.tagged_vlans.add(*form.cleaned_data['add_tagged_vlans'])
|
||||
if form.cleaned_data.get('remove_tagged_vlans', None):
|
||||
obj.tagged_vlans.remove(*form.cleaned_data['remove_tagged_vlans'])
|
||||
|
||||
|
||||
class InterfaceBulkRenameView(generic.BulkRenameView):
|
||||
queryset = Interface.objects.all()
|
||||
|
||||
@@ -24,7 +24,7 @@ __all__ = [
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
warnings.warn(
|
||||
f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||
from extras.models import ConfigTemplate
|
||||
from netbox.api.serializers import ValidatedModelSerializer
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import CustomLink
|
||||
from netbox.api.fields import ContentTypeField
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||
from core.models import ObjectType
|
||||
from extras.models import ExportTemplate
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import SavedFilter
|
||||
from netbox.api.fields import ContentTypeField
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import Tag
|
||||
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.module_loading import import_string
|
||||
from django_rq.queues import get_connection
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import status
|
||||
@@ -15,8 +14,8 @@ from rq import Worker
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras import filtersets
|
||||
from extras.models import *
|
||||
from extras.jobs import ScriptJob
|
||||
from extras.models import *
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.features import SyncedDataMixin
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
|
||||
@@ -6,7 +6,7 @@ class ExtrasConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import dashboard, lookups, search, signals
|
||||
from . import dashboard, lookups, search, signals # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
@@ -15,7 +15,6 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import BookmarkOrderingChoices
|
||||
from netbox.choices import ButtonColorChoices
|
||||
from utilities.object_types import object_type_identifier, object_type_name
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.querydict import dict_to_querydict
|
||||
|
||||
@@ -84,7 +84,7 @@ class CustomFieldType(ObjectType):
|
||||
class CustomFieldChoiceSetType(ObjectType):
|
||||
|
||||
choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]
|
||||
extra_choices: List[str] | None
|
||||
extra_choices: List[List[str]] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import traceback
|
||||
from contextlib import nullcontext
|
||||
from contextlib import ExitStack
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.signals import clear_events
|
||||
from extras.models import Script as ScriptModel
|
||||
from netbox.context_managers import event_tracking
|
||||
from netbox.jobs import JobRunner
|
||||
from netbox.registry import registry
|
||||
from utilities.exceptions import AbortScript, AbortTransaction
|
||||
from .utils import is_report
|
||||
|
||||
@@ -22,9 +22,7 @@ class ScriptJob(JobRunner):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
# An explicit job name is not set because it doesn't make sense in this context. Currently, there's no scenario
|
||||
# where jobs other than this one are used. Therefore, it is hidden, resulting in a cleaner job table overview.
|
||||
name = ''
|
||||
name = 'Run Script'
|
||||
|
||||
def run_script(self, script, request, data, commit):
|
||||
"""
|
||||
@@ -48,8 +46,7 @@ class ScriptJob(JobRunner):
|
||||
except AbortTransaction:
|
||||
script.log_info(message=_("Database changes have been reverted automatically."))
|
||||
if script.failed:
|
||||
logger.warning(f"Script failed")
|
||||
raise
|
||||
logger.warning("Script failed")
|
||||
|
||||
except Exception as e:
|
||||
if type(e) is AbortScript:
|
||||
@@ -103,5 +100,7 @@ class ScriptJob(JobRunner):
|
||||
|
||||
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
|
||||
# change logging, event rules, etc.
|
||||
with event_tracking(request) if commit else nullcontext():
|
||||
with ExitStack() as stack:
|
||||
for request_processor in registry['request_processors']:
|
||||
stack.enter_context(request_processor(request))
|
||||
self.run_script(script, request, data, commit)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db.models import CharField, TextField, Lookup
|
||||
from django.db.models import CharField, Lookup
|
||||
|
||||
from .fields import CachedValueField
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write("[*] Checking for latest release")
|
||||
if settings.ISOLATED_DEPLOYMENT:
|
||||
if options['verbosity']:
|
||||
self.stdout.write(f"\tSkipping: ISOLATED_DEPLOYMENT is enabled")
|
||||
self.stdout.write("\tSkipping: ISOLATED_DEPLOYMENT is enabled")
|
||||
elif settings.RELEASE_CHECK_URL:
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
@@ -129,7 +129,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(f"\tRequest error: {exc}", self.style.ERROR)
|
||||
else:
|
||||
if options['verbosity']:
|
||||
self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
|
||||
self.stdout.write("\tSkipping: RELEASE_CHECK_URL not set")
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Finished.", self.style.SUCCESS)
|
||||
|
||||
@@ -96,9 +96,9 @@ class Command(BaseCommand):
|
||||
if i:
|
||||
self.stdout.write(f'{i} entries cached.')
|
||||
else:
|
||||
self.stdout.write(f'No objects found.')
|
||||
self.stdout.write('No objects found.')
|
||||
|
||||
msg = f'Completed.'
|
||||
msg = 'Completed.'
|
||||
if total_count := search_backend.size:
|
||||
msg += f' Total entries: {total_count}'
|
||||
self.stdout.write(msg, self.style.SUCCESS)
|
||||
|
||||
@@ -51,7 +51,7 @@ class Command(BaseCommand):
|
||||
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
||||
|
||||
# Setup logging to Stdout
|
||||
formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
|
||||
formatter = logging.Formatter('[%(asctime)s][%(levelname)s] - %(message)s')
|
||||
stdouthandler = logging.StreamHandler(sys.stdout)
|
||||
stdouthandler.setLevel(logging.DEBUG)
|
||||
stdouthandler.setFormatter(formatter)
|
||||
|
||||
@@ -283,7 +283,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
"""
|
||||
for ct in content_types:
|
||||
model = ct.model_class()
|
||||
instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
|
||||
instances = model.objects.exclude(**{'custom_field_data__contains': self.name})
|
||||
for instance in instances:
|
||||
instance.custom_field_data[self.name] = self.default
|
||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||
|
||||
@@ -554,7 +554,7 @@ class BaseScript:
|
||||
"""
|
||||
Run the report and save its results. Each test method will be executed in order.
|
||||
"""
|
||||
self.logger.info(f"Running report")
|
||||
self.logger.info("Running report")
|
||||
|
||||
try:
|
||||
for test_name in self.tests:
|
||||
|
||||
@@ -12,7 +12,6 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Loca
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
||||
from netbox.events import *
|
||||
from users.models import Group, User
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
@@ -244,9 +243,18 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
choice_sets = (
|
||||
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']),
|
||||
CustomFieldChoiceSet(
|
||||
name='Choice Set 1',
|
||||
extra_choices=[['1A', '1A'], ['1B', '1B'], ['1C', '1C'], ['1D', '1D'], ['1E', '1E']],
|
||||
),
|
||||
CustomFieldChoiceSet(
|
||||
name='Choice Set 2',
|
||||
extra_choices=[['2A', '2A'], ['2B', '2B'], ['2C', '2C'], ['2D', '2D'], ['2E', '2E']],
|
||||
),
|
||||
CustomFieldChoiceSet(
|
||||
name='Choice Set 3',
|
||||
extra_choices=[['3A', '3A'], ['3B', '3B'], ['3C', '3C'], ['3D', '3D'], ['3E', '3E']],
|
||||
),
|
||||
)
|
||||
CustomFieldChoiceSet.objects.bulk_create(choice_sets)
|
||||
|
||||
@@ -784,7 +792,6 @@ class ScriptTest(APITestCase):
|
||||
super().setUp()
|
||||
|
||||
# Monkey-patch the Script model to return our TestScriptClass above
|
||||
from extras.api.views import ScriptViewSet
|
||||
Script.python_class = self.python_class
|
||||
|
||||
def test_get_script(self):
|
||||
|
||||
@@ -162,7 +162,7 @@ class CustomValidatorTest(TestCase):
|
||||
Site(name='abcdef123', slug='abcdef123').clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [region_validator]})
|
||||
def test_valid(self):
|
||||
def test_related_object(self):
|
||||
region1 = Region(name='Foo', slug='foo')
|
||||
region1.save()
|
||||
region2 = Region(name='Bar', slug='bar')
|
||||
|
||||