mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-25 19:28:15 +01:00
Compare commits
90 Commits
v4.2-beta1
...
v4.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f845b2cf07 | ||
|
|
2ed4a2b005 | ||
|
|
5b9210dfa5 | ||
|
|
4a13664e0f | ||
|
|
a9f3c74b0c | ||
|
|
50b7f46fc0 | ||
|
|
07ad4c1321 | ||
|
|
4a1fea3504 | ||
|
|
993d8f1480 | ||
|
|
c3efa2149c | ||
|
|
a75fa53d4d | ||
|
|
e75d327f38 | ||
|
|
a79d869bd8 | ||
|
|
32422d1683 | ||
|
|
571f604ce8 | ||
|
|
b12c8c880f | ||
|
|
b11f179527 | ||
|
|
80e1fd02bb | ||
|
|
4090afbf24 | ||
|
|
d04fc11c61 | ||
|
|
f6b8c1966d | ||
|
|
4456c488f1 | ||
|
|
53aa2c8624 | ||
|
|
ffac0974dd | ||
|
|
e518f08604 | ||
|
|
4ae5529362 | ||
|
|
ef6c89ee5d | ||
|
|
9c960c2387 | ||
|
|
ed541220e8 | ||
|
|
14cec518f5 | ||
|
|
9d82a668a4 | ||
|
|
b7610971c0 | ||
|
|
ab0a1f0bbc | ||
|
|
5d1070796d | ||
|
|
83d62315cc | ||
|
|
ab8fc3de5e | ||
|
|
67657efe1c | ||
|
|
c9ee699633 | ||
|
|
89d7487197 | ||
|
|
40f22533d1 | ||
|
|
c3b0de3ebd | ||
|
|
e8e3981da5 | ||
|
|
b9abb3200c | ||
|
|
10748edc3a | ||
|
|
6f4bec7644 | ||
|
|
0cda10a204 | ||
|
|
685264c757 | ||
|
|
f03489f58e | ||
|
|
c6452b33d8 | ||
|
|
16917133b2 | ||
|
|
28eada13d3 | ||
|
|
6ddd3cc779 | ||
|
|
1a631dd7cc | ||
|
|
8c07978042 | ||
|
|
7e3d8e9c3b | ||
|
|
e396097f3c | ||
|
|
8d6cec408c | ||
|
|
e7fcbffaf3 | ||
|
|
0b9ead3e8b | ||
|
|
13c26ccb0c | ||
|
|
aa56b99566 | ||
|
|
c0fec28b2a | ||
|
|
382e246b2c | ||
|
|
fff4ec78ad | ||
|
|
8951aa815f | ||
|
|
39ca3ce571 | ||
|
|
b89601d93d | ||
|
|
e63fe23af8 | ||
|
|
2da1a754c4 | ||
|
|
abfa28dc56 | ||
|
|
8e427e57ea | ||
|
|
dbaa9c1ce1 | ||
|
|
bd5e7a8d1a | ||
|
|
a15ff294dd | ||
|
|
26f8c3aae3 | ||
|
|
cc51e7032b | ||
|
|
0219dd7a70 | ||
|
|
edc9852229 | ||
|
|
001f06cc9a | ||
|
|
4017d0ca35 | ||
|
|
21962b3488 | ||
|
|
7a92c20576 | ||
|
|
3326a6543c | ||
|
|
674af4d6bc | ||
|
|
8c9bb73ff7 | ||
|
|
327ad8cfc9 | ||
|
|
1e845e6b46 | ||
|
|
b4265b74f4 | ||
|
|
954b5e9ddf | ||
|
|
d122c334fd |
15
.github/ISSUE_TEMPLATE/01-feature_request.yaml
vendored
15
.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.7
|
||||
placeholder: v4.2.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -27,19 +27,6 @@ body:
|
||||
- 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
|
||||
attributes:
|
||||
label: Proposed functionality
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
15
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -22,24 +22,11 @@ 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.7
|
||||
placeholder: v4.2.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,7 +2,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📖 Contributing Policy
|
||||
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
||||
url: https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md
|
||||
about: "Please read through our contributing policy before opening an issue or pull request."
|
||||
- name: ❓ Discussion
|
||||
url: https://github.com/netbox-community/netbox/discussions
|
||||
|
||||
2
.github/workflows/close-stale-issues.yml
vendored
2
.github/workflows/close-stale-issues.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
issues may receive direct feedback. **Do not** attempt to circumvent this
|
||||
process by "bumping" the issue; doing so will result in its immediate closure
|
||||
and you may be barred from participating in any future discussions. Please see
|
||||
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
||||
our [contributing guide](https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Pull request parameters
|
||||
close-pr-message: >
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/main/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>
|
||||
<a href="https://github.com/netbox-community/netbox/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
||||
<p>
|
||||
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
|
||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||
|
||||
@@ -8,8 +8,6 @@ django-cors-headers
|
||||
|
||||
# Runtime UI tool for debugging Django
|
||||
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
|
||||
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
|
||||
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
|
||||
django-debug-toolbar
|
||||
|
||||
# Library for writing reusable URL query filters
|
||||
@@ -101,7 +99,7 @@ netaddr
|
||||
nh3
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
|
||||
# https://github.com/python-pillow/Pillow/releases
|
||||
Pillow
|
||||
|
||||
# PostgreSQL database adapter for Python
|
||||
@@ -134,7 +132,8 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
strawberry-graphql-django
|
||||
# Pinned to v0.52.0 for suspected upstream bug; see #18329
|
||||
strawberry-graphql-django==0.52.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
@@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
|
||||
|
||||
## DATABASE
|
||||
|
||||
NetBox requires access to a PostgreSQL 12 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
NetBox requires access to a PostgreSQL 13 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
|
||||
* `NAME` - Database name
|
||||
* `USER` - PostgreSQL username
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -37,16 +37,12 @@ CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scri
|
||||
|
||||
### 2. Create a New Branch
|
||||
|
||||
The NetBox project utilizes three persistent git branches to track work:
|
||||
The NetBox project utilizes two persistent git branches to track work:
|
||||
|
||||
* `master` - Serves as a snapshot of the current stable release
|
||||
* `develop` - All development on the upcoming stable (patch) release occurs here
|
||||
* `feature` - Tracks work on an upcoming minor release
|
||||
* `main` - All development on the upcoming stable (patch) release occurs here. Releases are published from this branch.
|
||||
* `feature` - All work planned for the upcoming minor release is done here.
|
||||
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
|
||||
|
||||
!!! warning
|
||||
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
Typically, you'll base pull requests off of the `main` branch, or off of `feature` if you're working on the upcoming minor or major release. For example, assume that the current NetBox release is v4.2.3. Work applied to the `main` branch will appear in v4.2.4, and work done under the `feature` branch will be included in the next minor release (v4.3.0).
|
||||
|
||||
To create a new branch, first ensure that you've checked out the desired base branch, then run:
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Fast-forward
|
||||
```
|
||||
|
||||
!!! warning "Avoid Merging Remote Branches"
|
||||
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||
You generally want to avoid merging branches that exist on the remote (upstream) repository, namely `main` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||
|
||||
### Show Pending Changes
|
||||
|
||||
@@ -196,7 +196,7 @@ index 93e125079..4344fb514 100644
|
||||
+and here too
|
||||
+
|
||||
<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/main/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
diff --git a/foo.py b/foo.py
|
||||
new file mode 100644
|
||||
|
||||
@@ -8,11 +8,10 @@ NetBox and many of its related projects are maintained on [GitHub](https://githu
|
||||
|
||||

|
||||
|
||||
There are three permanent branches in the repository:
|
||||
There are two permanent branches in the repository:
|
||||
|
||||
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
|
||||
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
|
||||
* `main` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v4.2 to v4.3).
|
||||
|
||||
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
|
||||
|
||||
@@ -57,4 +56,4 @@ NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevo
|
||||
|
||||
## Licensing
|
||||
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/main/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
|
||||
@@ -43,9 +43,9 @@ Follow these instructions to perform a new installation of NetBox in a temporary
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
|
||||
### Merge the Release Branch
|
||||
### Merge the `feature` Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
@@ -55,6 +55,15 @@ After the release of a new minor version, generate a new demo data snapshot comp
|
||||
|
||||
## Patch Releases
|
||||
|
||||
### Create a Release Branch
|
||||
|
||||
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
|
||||
|
||||
```
|
||||
git checkout main
|
||||
git checkout -B release-vX.Y.Z
|
||||
```
|
||||
|
||||
### Notify netbox-docker Project of Any Relevant Changes
|
||||
|
||||
Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including:
|
||||
@@ -111,25 +120,19 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
Commit these changes to the `develop` branch and push upstream.
|
||||
|
||||
### Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceeding with the release.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.
|
||||
|
||||
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
|
||||
Once CI has completed and a colleague has reviewed the PR, merge it. This effects a new release in the `main` branch.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
||||
* **Tag:** Current version (e.g. `v3.3.1`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
|
||||
* **Tag:** Current version (e.g. `v4.2.1`)
|
||||
* **Target:** `main`
|
||||
* **Title:** Version and date (e.g. `v4.2.1 - 2025-01-17`)
|
||||
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
|
||||
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
@@ -14,10 +14,10 @@ To update the English `.po` file from which all translations are derived, use th
|
||||
./manage.py makemessages -l en -i "project-static/*"
|
||||
```
|
||||
|
||||
Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
|
||||
Then, commit the change and push to the `main` 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).
|
||||
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/main/.github/workflows/update-translation-strings.yml).
|
||||
|
||||
## Updating Translated Strings
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
|
||||
|
||||
!!! warning "PostgreSQL 12 or later required"
|
||||
NetBox requires PostgreSQL 12 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
!!! warning "PostgreSQL 13 or later required"
|
||||
NetBox requires PostgreSQL 13 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -34,7 +34,7 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
sudo systemctl enable --now postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 12 or later:
|
||||
Before continuing, verify that you have installed PostgreSQL 13 or later:
|
||||
|
||||
```no-highlight
|
||||
psql -V
|
||||
|
||||
@@ -29,7 +29,7 @@ python3 -V
|
||||
|
||||
## Download NetBox
|
||||
|
||||
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by re-pulling the `master` branch.
|
||||
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by checking out the latest release tag.
|
||||
|
||||
### Option A: Download a Release Archive
|
||||
|
||||
@@ -67,16 +67,13 @@ If `git` is not already installed, install it:
|
||||
sudo yum install -y git
|
||||
```
|
||||
|
||||
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
|
||||
Next, clone the git repository:
|
||||
|
||||
```no-highlight
|
||||
sudo git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
|
||||
sudo git clone https://github.com/netbox-community/netbox.git .
|
||||
```
|
||||
|
||||
!!! note
|
||||
The `git clone` command above utilizes a "shallow clone" to retrieve only the most recent commit. If you need to download the entire history, omit the `--depth 1` argument.
|
||||
|
||||
The `git clone` command should generate output similar to the following:
|
||||
This command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
@@ -88,8 +85,13 @@ Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
||||
Resolving deltas: 100% (148/148), done.
|
||||
```
|
||||
|
||||
!!! note
|
||||
Installation via git also allows you to easily try out different versions of NetBox. To check out a [specific NetBox release](https://github.com/netbox-community/netbox/releases), use the `git checkout` command with the desired release tag. For example, `git checkout v3.0.8`.
|
||||
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
|
||||
|
||||
```
|
||||
sudo git checkout vX.Y.Z
|
||||
```
|
||||
|
||||
Using this installation method enables easy upgrades in the future by simply checking out the latest release tag.
|
||||
|
||||
## Create the NetBox System User
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
Below is a simplified overview of the NetBox application stack for reference:
|
||||
|
||||
@@ -20,15 +20,15 @@ NetBox requires the following dependencies:
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 12+ |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by checking out the latest production release from the git repository.
|
||||
|
||||
!!! warning
|
||||
Use the same method as you used to install NetBox originally
|
||||
Use the same method as you used to install NetBox originally.
|
||||
|
||||
If you are not sure how NetBox was installed originally, check with this command:
|
||||
|
||||
@@ -36,10 +36,7 @@ If you are not sure how NetBox was installed originally, check with this command
|
||||
ls -ld /opt/netbox /opt/netbox/.git
|
||||
```
|
||||
|
||||
If NetBox was installed from a release package, then `/opt/netbox` will be a
|
||||
symlink pointing to the current version, and `/opt/netbox/.git` will not
|
||||
exist. If it was installed from git, then `/opt/netbox` and
|
||||
`/opt/netbox/.git` will both exist as normal directories.
|
||||
If NetBox was installed from a release package, then `/opt/netbox` will be a symlink pointing to the current version, and `/opt/netbox/.git` will not exist. If it was installed from git, then `/opt/netbox` and `/opt/netbox/.git` will both exist as normal directories.
|
||||
|
||||
### Option A: Download a Release
|
||||
|
||||
@@ -84,20 +81,20 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
||||
sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
|
||||
```
|
||||
|
||||
### Option B: Clone the Git Repository
|
||||
### Option B: Check Out a Git Release
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following `git` commands:
|
||||
|
||||
```no-highlight
|
||||
cd /opt/netbox
|
||||
sudo git checkout master
|
||||
sudo git pull origin master
|
||||
```
|
||||
sudo git fetch --tags
|
||||
git describe --tags $(git rev-list --tags --max-count=1)
|
||||
```
|
||||
|
||||
!!! info "Checking out an older release"
|
||||
If you need to upgrade to an older version rather than the current stable release, you can check out any valid [git tag](https://github.com/netbox-community/netbox/tags), each of which represents a release. For example, to checkout the code for NetBox v2.11.11, do:
|
||||
Check out the desired release by specifying its tag:
|
||||
|
||||
sudo git checkout v2.11.11
|
||||
```
|
||||
sudo git checkout v4.2.0
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
|
||||
|
||||
@@ -79,5 +79,5 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| HTTP service | nginx or Apache |
|
||||
| WSGI service | gunicorn or uWSGI |
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 12+ |
|
||||
| Database | PostgreSQL 13+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
|
||||
@@ -8,9 +8,9 @@ Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation
|
||||
|
||||
The [circuit group](./circuitgroup.md) being assigned.
|
||||
|
||||
### Circuit
|
||||
### Member
|
||||
|
||||
The [circuit](./circuit.md) that is being assigned to the group.
|
||||
The [circuit](./circuit.md) or [virtual circuit](./virtualcircuit.md) assigned to the group.
|
||||
|
||||
### Priority
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ The [provider account](./provideraccount.md) with which the virtual circuit is a
|
||||
|
||||
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
|
||||
|
||||
### Type
|
||||
|
||||
The assigned [virtual circuit type](./virtualcircuittype.md).
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the virtual circuit. By default, the following statuses are available:
|
||||
|
||||
13
docs/models/circuits/virtualcircuittype.md
Normal file
13
docs/models/circuits/virtualcircuittype.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Virtual Circuit Types
|
||||
|
||||
Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
@@ -10,7 +10,7 @@ See the [event rules documentation](../../features/event-rules.md) for more inf
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Content Types
|
||||
### Object Types
|
||||
|
||||
The type(s) of object in NetBox that will trigger the rule.
|
||||
|
||||
@@ -38,3 +38,15 @@ The event types which will trigger the rule. At least one event type must be sel
|
||||
### Conditions
|
||||
|
||||
A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, no action will be taken. An event rule that does not define any conditions will _always_ trigger.
|
||||
|
||||
### Action Type
|
||||
|
||||
The type of action to take when the rule triggers. This must be one of the following choices:
|
||||
|
||||
* Webhook
|
||||
* Custom script
|
||||
* Notification
|
||||
|
||||
### Action Data
|
||||
|
||||
An optional dictionary of JSON data to pass when executing the rule. This can be useful to include additional context data, e.g. when transmitting a webhook.
|
||||
|
||||
@@ -1,6 +1,71 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.7 (FUTURE)
|
||||
## v4.1.11 (2025-01-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17771](https://github.com/netbox-community/netbox/issues/17771) - Fix duplicate entries appearing on VLAN list when filtering by interface assignment
|
||||
* [#18222](https://github.com/netbox-community/netbox/issues/18222) - Pass event rule action data to webhooks as context data
|
||||
* [#18263](https://github.com/netbox-community/netbox/issues/18263) - Fix recalculation of cable paths when modifying cable terminations via the REST API
|
||||
* [#18271](https://github.com/netbox-community/netbox/issues/18271) - Require only encryption _or_ authentication algorithm when creating an IPSec proposal via the REST API
|
||||
* [#18289](https://github.com/netbox-community/netbox/issues/18289) - Enable ordering modules and module types by created & last updated times
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
# NetBox v4.2
|
||||
|
||||
## v4.2-beta1 (2024-12-02)
|
||||
## v4.2.2 (2025-01-17)
|
||||
|
||||
!!! danger "Not for Production Use"
|
||||
This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
|
||||
### Bug Fixes
|
||||
|
||||
* [#18336](https://github.com/netbox-community/netbox/issues/18336) - Validate new rack height against installed devices when changing a rack's type
|
||||
* [#18350](https://github.com/netbox-community/netbox/issues/18350) - Fix `FieldDoesNotExist` exception when global search results include a circuit termination
|
||||
* [#18353](https://github.com/netbox-community/netbox/issues/18353) - Disable fetching of plugin catalog data when `ISOLATED_DEPLOYMENT` is enabled
|
||||
* [#18362](https://github.com/netbox-community/netbox/issues/18362) - Avoid transmitting census data on every worker restart
|
||||
* [#18363](https://github.com/netbox-community/netbox/issues/18363) - Fix support for assigning a MAC address to an interface via the REST API
|
||||
* [#18368](https://github.com/netbox-community/netbox/issues/18368) - Restore missing attributes from REST API serializer for MAC addresses (`tags`, `created`, `last_updated`, and custom fields)
|
||||
* [#18369](https://github.com/netbox-community/netbox/issues/18369) - Fix `TypeError` exception when rendering the system configuration view with one or more custom classes defined under `PROTECTION_RULES`
|
||||
* [#18373](https://github.com/netbox-community/netbox/issues/18373) - Fix `AttributeError` exception when attempting to assign host devices to a cluster
|
||||
* [#18376](https://github.com/netbox-community/netbox/issues/18376) - Fix the display of tagged VLANs in interfaces list for Q-in-Q interfaces
|
||||
* [#18379](https://github.com/netbox-community/netbox/issues/18379) - Ensure RSS feed dashboard widget content is sanitized
|
||||
* [#18392](https://github.com/netbox-community/netbox/issues/18392) - Virtual machines should not inherit config contexts assigned to locations
|
||||
* [#18400](https://github.com/netbox-community/netbox/issues/18400) - Fix support for `STORAGE_BACKEND` configuration parameter
|
||||
* [#18406](https://github.com/netbox-community/netbox/issues/18406) - Scope column headers in object lists should not be orderable
|
||||
|
||||
---
|
||||
|
||||
## v4.2.1 (2025-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18282](https://github.com/netbox-community/netbox/issues/18282) - Fix ordering of prefixes list by assigned VLAN
|
||||
* [#18314](https://github.com/netbox-community/netbox/issues/18314) - Fix KeyError exception when rendering pre-saved dashboard (`requires_internet` missing)
|
||||
* [#18316](https://github.com/netbox-community/netbox/issues/18316) - Fix AttributeError exception when global search results include prefixes and/or clusters
|
||||
* [#18318](https://github.com/netbox-community/netbox/issues/18318) - Correct navigation breadcrumbs for module type UI view
|
||||
* [#18324](https://github.com/netbox-community/netbox/issues/18324) - Correct filtering for certain related object listings
|
||||
* [#18329](https://github.com/netbox-community/netbox/issues/18329) - Address upstream bug in GraphQL API where only one primary IP address is returned within a device/VM list
|
||||
|
||||
---
|
||||
|
||||
## v4.2.0 (2025-01-06)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Support for the Django admin UI has been completely removed. (The Django admin UI was disabled by default in NetBox v4.0.)
|
||||
* This release drops support for PostgreSQL 12. PostgreSQL 13 or later is required to run this release.
|
||||
* NetBox has adopted collation-based natural ordering for many models. This may alter the order in which some objects are listed by default.
|
||||
* Automatic redirects from pre-v4.1 UI views for virtual disks have been removed.
|
||||
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
|
||||
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
|
||||
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
|
||||
* The `circuit` foreign key field on `circuits.CircuitGroupAssignment` has been replaced by the `member` generic foreign key.
|
||||
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
|
||||
|
||||
### New Features
|
||||
@@ -77,6 +109,8 @@ NetBox now supports the designation of customer VLANs (CVLANs) and service VLANs
|
||||
* `/api/ipam/vlan-translation-rules/`
|
||||
* circuits.Circuit
|
||||
* Added the optional `distance` and `distance_unit` fields
|
||||
* circuits.CircuitGroupAssignment
|
||||
* Replaced the `circuit` field with `member_type` and `member_id` to support virtual circuit assignment
|
||||
* circuits.CircuitTermination
|
||||
* Removed the `site` & `provider_network` fields
|
||||
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
|
||||
|
||||
@@ -176,6 +176,7 @@ nav:
|
||||
- Provider Network: 'models/circuits/providernetwork.md'
|
||||
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
|
||||
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
|
||||
- Virtual Circuit Type: 'models/circuits/virtualcircuittype.md'
|
||||
- Core:
|
||||
- DataFile: 'models/core/datafile.md'
|
||||
- DataSource: 'models/core/datasource.md'
|
||||
|
||||
@@ -3,10 +3,10 @@ from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import (
|
||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||
VirtualCircuitTermination,
|
||||
VirtualCircuitTermination, VirtualCircuitType,
|
||||
)
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
@@ -15,7 +15,6 @@ from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializ
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
__all__ = (
|
||||
@@ -26,6 +25,7 @@ __all__ = (
|
||||
'CircuitTypeSerializer',
|
||||
'VirtualCircuitSerializer',
|
||||
'VirtualCircuitTerminationSerializer',
|
||||
'VirtualCircuitTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
@@ -154,27 +154,54 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
member_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
|
||||
)
|
||||
member = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_member(self, obj):
|
||||
if obj.member_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.member)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.member, nested=True, context=context).data
|
||||
|
||||
|
||||
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'virtual_circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
||||
|
||||
|
||||
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
||||
provider_network = ProviderNetworkSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
type = VirtualCircuitTypeSerializer(nested=True)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'status', 'tenant',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
|
||||
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet
|
||||
|
||||
# Virtual circuits
|
||||
router.register('virtual-circuits', views.VirtualCircuitViewSet)
|
||||
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
|
||||
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
|
||||
@@ -95,6 +95,16 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit types
|
||||
#
|
||||
|
||||
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTypeSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
)
|
||||
|
||||
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
|
||||
app_label='circuits',
|
||||
model__in=['circuit', 'virtualcircuit']
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -7,7 +8,9 @@ from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -22,6 +25,7 @@ __all__ = (
|
||||
'ProviderFilterSet',
|
||||
'VirtualCircuitFilterSet',
|
||||
'VirtualCircuitTerminationFilterSet',
|
||||
'VirtualCircuitTypeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@@ -365,26 +369,36 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
member_type = ContentTypeFilter()
|
||||
circuit = MultiValueCharFilter(
|
||||
method='filter_circuit',
|
||||
field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
circuit_id = MultiValueNumberFilter(
|
||||
method='filter_circuit',
|
||||
field_name='pk',
|
||||
label=_('Circuit (ID)'),
|
||||
)
|
||||
circuit = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__cid',
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
virtual_circuit = MultiValueCharFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='cid',
|
||||
label=_('Virtual circuit (CID)'),
|
||||
)
|
||||
virtual_circuit_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='pk',
|
||||
label=_('Virtual circuit (ID)'),
|
||||
)
|
||||
provider = MultiValueCharFilter(
|
||||
method='filter_provider',
|
||||
field_name='slug',
|
||||
label=_('Provider (name)'),
|
||||
)
|
||||
provider_id = MultiValueNumberFilter(
|
||||
method='filter_provider',
|
||||
field_name='pk',
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
@@ -399,16 +413,62 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('id', 'priority')
|
||||
fields = ('id', 'member_id', 'priority')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(member__cid__icontains=value) |
|
||||
Q(group__name__icontains=value)
|
||||
)
|
||||
|
||||
def filter_circuit(self, queryset, name, value):
|
||||
circuits = Circuit.objects.filter(**{f'{name}__in': value})
|
||||
if not circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_virtual_circuit(self, queryset, name, value):
|
||||
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
|
||||
if not virtual_circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_provider(self, queryset, name, value):
|
||||
providers = Provider.objects.filter(**{f'{name}__in': value})
|
||||
if not providers.exists():
|
||||
return queryset.none()
|
||||
circuits = Circuit.objects.filter(provider__in=providers)
|
||||
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
) |
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -437,6 +497,16 @@ class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
label=_('Virtual circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Virtual circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
|
||||
@@ -32,6 +32,7 @@ __all__ = (
|
||||
'ProviderNetworkBulkEditForm',
|
||||
'VirtualCircuitBulkEditForm',
|
||||
'VirtualCircuitTerminationBulkEditForm',
|
||||
'VirtualCircuitTypeBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -279,7 +280,7 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
circuit = DynamicModelChoiceField(
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
@@ -292,11 +293,29 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('circuit', 'priority'),
|
||||
FieldSet('member', 'priority'),
|
||||
)
|
||||
nullable_fields = ('priority',)
|
||||
|
||||
|
||||
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
@@ -308,6 +327,11 @@ class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
|
||||
@@ -24,6 +24,7 @@ __all__ = (
|
||||
'VirtualCircuitImportForm',
|
||||
'VirtualCircuitTerminationImportForm',
|
||||
'VirtualCircuitTerminationImportRelatedForm',
|
||||
'VirtualCircuitTypeImportForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -179,10 +180,27 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
member_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
label=_('Circuit type (app & model)')
|
||||
)
|
||||
priority = CSVChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=CircuitPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('circuit', 'group', 'priority')
|
||||
fields = ('member_type', 'member_id', 'group', 'priority')
|
||||
|
||||
|
||||
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
|
||||
|
||||
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||
@@ -199,6 +217,12 @@ class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||
help_text=_('Assigned provider account (if any)'),
|
||||
required=False
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Type of virtual circuit')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
@@ -215,7 +239,8 @@ class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'description', 'comments', 'tags',
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ __all__ = (
|
||||
'ProviderNetworkFilterForm',
|
||||
'VirtualCircuitFilterForm',
|
||||
'VirtualCircuitTerminationFilterForm',
|
||||
'VirtualCircuitTypeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -277,14 +278,14 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
member_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
@@ -302,12 +303,26 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('status', name=_('Attributes')),
|
||||
FieldSet('type', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
@@ -332,6 +347,11 @@ class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBox
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False,
|
||||
label=_('Type')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
|
||||
@@ -31,6 +31,7 @@ __all__ = (
|
||||
'ProviderNetworkForm',
|
||||
'VirtualCircuitForm',
|
||||
'VirtualCircuitTerminationForm',
|
||||
'VirtualCircuitTypeForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -251,16 +252,71 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
|
||||
label=_('Group'),
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
member_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Circuit type')
|
||||
)
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
queryset=Circuit.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'group', 'circuit', 'priority', 'tags',
|
||||
'group', 'member_type', 'priority', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.member:
|
||||
initial['member'] = instance.member
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if member_type_id := get_field_value(self, 'member_type'):
|
||||
try:
|
||||
model = ContentType.objects.get(pk=member_type_id).model_class()
|
||||
self.fields['member'].queryset = model.objects.all()
|
||||
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['member'].disabled = False
|
||||
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance.pk and member_type_id != self.instance.member_type_id:
|
||||
self.initial['member'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected circuit (if any)
|
||||
self.instance.member = self.cleaned_data.get('member')
|
||||
|
||||
|
||||
class VirtualCircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -275,11 +331,16 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'provider_network', 'provider_account', 'cid', 'status', 'description', 'tags', name=_('Virtual circuit'),
|
||||
'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
|
||||
name=_('Virtual circuit'),
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@@ -287,7 +348,7 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ __all__ = (
|
||||
'ProviderNetworkFilter',
|
||||
'VirtualCircuitFilter',
|
||||
'VirtualCircuitTerminationFilter',
|
||||
'VirtualCircuitTypeFilter',
|
||||
)
|
||||
|
||||
|
||||
@@ -65,6 +66,12 @@ class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
|
||||
class VirtualCircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
|
||||
class VirtualCircuitFilter(BaseFilterMixin):
|
||||
|
||||
@@ -37,3 +37,6 @@ class CircuitsQuery:
|
||||
|
||||
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
|
||||
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@@ -21,6 +21,7 @@ __all__ = (
|
||||
'ProviderNetworkType',
|
||||
'VirtualCircuitTerminationType',
|
||||
'VirtualCircuitType',
|
||||
'VirtualCircuitTypeType',
|
||||
)
|
||||
|
||||
|
||||
@@ -116,12 +117,29 @@ class CircuitGroupType(OrganizationalObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
fields='__all__',
|
||||
exclude=('member_type', 'member_id'),
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
@strawberry_django.field
|
||||
def member(self) -> Annotated[Union[
|
||||
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
|
||||
return self.member
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitType,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTypeFilter
|
||||
)
|
||||
class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
@@ -148,6 +166,9 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
class VirtualCircuitType(NetBoxObjectType):
|
||||
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||
provider_account: ProviderAccountType | None
|
||||
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
|
||||
select_related=["type"]
|
||||
)
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[VirtualCircuitTerminationType]
|
||||
|
||||
@@ -2,6 +2,7 @@ import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
import utilities.json
|
||||
|
||||
|
||||
@@ -14,6 +15,29 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
encoder=utilities.json.CustomFieldJSONEncoder
|
||||
)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('color', utilities.fields.ColorField(blank=True, max_length=6)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit type',
|
||||
'verbose_name_plural': 'virtual circuit types',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuit',
|
||||
fields=[
|
||||
@@ -47,6 +71,14 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.virtualcircuittype'
|
||||
)
|
||||
),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_member_type(apps, schema_editor):
|
||||
"""
|
||||
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
|
||||
CircuitGroupAssignment.objects.update(
|
||||
member_type=ContentType.objects.get_for_model(Circuit)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0050_virtual_circuits'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
name='circuits_circuitgroupassignment_unique_circuit_group',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='circuitgroupassignment',
|
||||
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
|
||||
),
|
||||
|
||||
# Change member_id to an integer field for the member GFK
|
||||
migrations.RenameField(
|
||||
model_name='circuitgroupassignment',
|
||||
old_name='circuit',
|
||||
new_name='member_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_id',
|
||||
field=models.PositiveBigIntegerField(),
|
||||
),
|
||||
|
||||
# Add content type pointer for the member GFK
|
||||
migrations.AddField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
blank=True,
|
||||
null=True
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
|
||||
# Populate member_type for any existing assignments
|
||||
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
|
||||
|
||||
# Disallow null values for member_type
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
|
||||
migrations.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='circuits_circuitgroupassignment_unique_member_group'
|
||||
),
|
||||
),
|
||||
]
|
||||
23
netbox/circuits/models/base.py
Normal file
23
netbox/circuits/models/base.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import OrganizationalModel
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitType',
|
||||
)
|
||||
|
||||
|
||||
class BaseCircuitType(OrganizationalModel):
|
||||
"""
|
||||
Abstract base model to represent a type of physical or virtual circuit.
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -14,7 +13,7 @@ from netbox.models.mixins import DistanceMixin
|
||||
from netbox.models.features import (
|
||||
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
)
|
||||
from utilities.fields import ColorField
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@@ -25,16 +24,11 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CircuitType(OrganizationalModel):
|
||||
class CircuitType(BaseCircuitType):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit type')
|
||||
@@ -65,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='CircuitType',
|
||||
to='circuits.CircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
@@ -117,6 +111,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
||||
null=True
|
||||
)
|
||||
|
||||
group_assignments = GenericRelation(
|
||||
to='circuits.CircuitGroupAssignment',
|
||||
content_type_field='member_type',
|
||||
object_id_field='member_id',
|
||||
related_query_name='circuit'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description',
|
||||
@@ -177,15 +178,23 @@ class CircuitGroup(OrganizationalModel):
|
||||
|
||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
"""
|
||||
Assignment of a Circuit to a CircuitGroup with an optional priority.
|
||||
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
|
||||
"""
|
||||
circuit = models.ForeignKey(
|
||||
Circuit,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
member_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
member_id = models.PositiveBigIntegerField(
|
||||
verbose_name=_('member ID')
|
||||
)
|
||||
member = GenericForeignKey(
|
||||
ct_field='member_type',
|
||||
fk_field='member_id'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
CircuitGroup,
|
||||
to='circuits.CircuitGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
@@ -197,16 +206,15 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
null=True
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.Circuit',
|
||||
'circuits.CircuitGroup',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('group', 'circuit', 'priority', 'pk')
|
||||
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('circuit', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_circuit_group'
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_member_group'
|
||||
),
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -8,13 +9,26 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.choices import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
'VirtualCircuit',
|
||||
'VirtualCircuitTermination',
|
||||
'VirtualCircuitType',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitType(BaseCircuitType):
|
||||
"""
|
||||
Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
|
||||
to categorize virtual circuits by their technological nature or by product name.
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('virtual circuit type')
|
||||
verbose_name_plural = _('virtual circuit types')
|
||||
|
||||
|
||||
class VirtualCircuit(PrimaryModel):
|
||||
"""
|
||||
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
||||
@@ -36,6 +50,11 @@ class VirtualCircuit(PrimaryModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='circuits.VirtualCircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits'
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
@@ -50,11 +69,19 @@ class VirtualCircuit(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
group_assignments = GenericRelation(
|
||||
to='circuits.CircuitGroupAssignment',
|
||||
content_type_field='member_type',
|
||||
object_id_field='member_id',
|
||||
related_query_name='virtual_circuit'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.ProviderNetwork',
|
||||
'circuits.VirtualCircuitType',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -34,7 +34,7 @@ class CircuitTerminationIndex(SearchIndex):
|
||||
('port_speed', 2000),
|
||||
('upstream_speed', 2000),
|
||||
)
|
||||
display_attrs = ('circuit', 'site', 'provider_network', 'description')
|
||||
display_attrs = ('circuit', 'termination', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@@ -100,3 +100,14 @@ class VirtualCircuitTerminationIndex(SearchIndex):
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('virtual_circuit', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitTypeIndex(SearchIndex):
|
||||
model = models.VirtualCircuitType
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -45,7 +45,7 @@ class CircuitTypeTable(NetBoxTable):
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
||||
|
||||
|
||||
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
@@ -61,6 +61,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
@@ -188,11 +192,14 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor='circuit__provider',
|
||||
accessor='member__provider',
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
circuit = tables.Column(
|
||||
member_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
member = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
)
|
||||
@@ -206,6 +213,7 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroupAssignment
|
||||
fields = (
|
||||
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
||||
'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
|
||||
'tags',
|
||||
)
|
||||
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
|
||||
default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')
|
||||
|
||||
@@ -8,9 +8,34 @@ from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
__all__ = (
|
||||
'VirtualCircuitTable',
|
||||
'VirtualCircuitTerminationTable',
|
||||
'VirtualCircuitTypeTable',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTypeTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:virtualcircuittype_list'
|
||||
)
|
||||
virtual_circuit_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:virtualcircuit_list',
|
||||
url_params={'type_id': 'pk'},
|
||||
verbose_name=_('Circuits')
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
@@ -29,6 +54,10 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:virtualcircuittermination_list',
|
||||
@@ -45,12 +74,12 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'tenant_group',
|
||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'termination_count',
|
||||
'description',
|
||||
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||
'termination_count', 'description',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroupAssignment
|
||||
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
|
||||
brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url']
|
||||
bulk_update_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
@@ -330,17 +330,17 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
@@ -349,17 +349,20 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
cls.create_data = [
|
||||
{
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[4].pk,
|
||||
'circuit': circuits[4].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[4].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[5].pk,
|
||||
'circuit': circuits[5].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[5].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
||||
},
|
||||
]
|
||||
@@ -406,6 +409,38 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuitType
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count']
|
||||
create_data = (
|
||||
{
|
||||
'name': 'Virtual Circuit Type 4',
|
||||
'slug': 'virtual-circuit-type-4',
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Circuit Type 5',
|
||||
'slug': 'virtual-circuit-type-5',
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Circuit Type 6',
|
||||
'slug': 'virtual-circuit-type-6',
|
||||
},
|
||||
)
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
|
||||
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
|
||||
@@ -418,21 +453,28 @@ class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
)
|
||||
@@ -443,18 +485,21 @@ class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
'cid': 'Virtual Circuit 4',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 5',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 6',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
]
|
||||
@@ -560,27 +605,35 @@ class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1'
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2'
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3'
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4'
|
||||
cid='Virtual Circuit 4',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
@@ -648,7 +648,6 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
@@ -656,43 +655,86 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
))
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type),
|
||||
Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type),
|
||||
Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
member=virtual_circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
member=virtual_circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
member=virtual_circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
def test_group_id(self):
|
||||
groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
|
||||
def test_group(self):
|
||||
groups = CircuitGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_circuit(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
@@ -701,12 +743,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_circuit(self):
|
||||
virtual_circuits = VirtualCircuit.objects.all()[:2]
|
||||
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@@ -795,6 +844,36 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
filterset = VirtualCircuitTypeFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
VirtualCircuitType.objects.bulk_create((
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Virtual Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['virtual-circuit-type-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
filterset = VirtualCircuitFilterSet
|
||||
@@ -838,12 +917,20 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
virutal_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
tenant=tenants[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_types[0],
|
||||
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||
description='virtualcircuit1',
|
||||
),
|
||||
@@ -852,6 +939,7 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
provider_account=provider_accounts[1],
|
||||
tenant=tenants[1],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_types[1],
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
description='virtualcircuit2',
|
||||
),
|
||||
@@ -860,6 +948,7 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
provider_account=provider_accounts[2],
|
||||
tenant=tenants[2],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_types[2],
|
||||
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
|
||||
description='virtualcircuit3',
|
||||
),
|
||||
@@ -891,6 +980,13 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
virtual_circuit_types = VirtualCircuitType.objects.all()[:2]
|
||||
params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -987,22 +1083,29 @@ class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ProviderAccount(provider=providers[2], account='Provider Account 3'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1'
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
provider_account=provider_accounts[1],
|
||||
cid='Virtual Circuit 2'
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
provider_account=provider_accounts[2],
|
||||
cid='Virtual Circuit 3'
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
@@ -468,6 +468,7 @@ class CircuitGroupAssignmentTestCase(
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = CircuitGroupAssignment
|
||||
@@ -497,17 +498,17 @@ class CircuitGroupAssignmentTestCase(
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
circuit=circuits[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
circuit=circuits[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
circuit=circuits[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
@@ -517,16 +518,72 @@ class CircuitGroupAssignmentTestCase(
|
||||
|
||||
cls.form_data = {
|
||||
'group': circuit_groups[3].pk,
|
||||
'circuit': circuits[3].pk,
|
||||
'member_type': ContentType.objects.get_for_model(Circuit).pk,
|
||||
'member': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"member_type,member_id,group,priority",
|
||||
f"circuits.circuit,{circuits[0].pk},{circuit_groups[3].pk},primary",
|
||||
f"circuits.circuit,{circuits[1].pk},{circuit_groups[3].pk},secondary",
|
||||
f"circuits.circuit,{circuits[2].pk},{circuit_groups[3].pk},tertiary",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,priority",
|
||||
f"{assignments[0].pk},inactive",
|
||||
f"{assignments[1].pk},inactive",
|
||||
f"{assignments[2].pk},inactive",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = VirtualCircuitType
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Virtual Circuit Type X',
|
||||
'slug': 'virtual-circuit-type-x',
|
||||
'description': 'A new virtual circuit type',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Virtual Circuit Type 4,circuit-type-4",
|
||||
"Virtual Circuit Type 5,circuit-type-5",
|
||||
"Virtual Circuit Type 6,circuit-type-6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7",
|
||||
f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8",
|
||||
f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualCircuit
|
||||
|
||||
@@ -550,22 +607,30 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
ProviderAccount(provider=provider, account='Provider Account 2'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1'
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 2'
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 3'
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
@@ -584,6 +649,7 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'cid': 'Virtual Circuit X',
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': virtual_circuit_types[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
'description': 'A new virtual circuit',
|
||||
'comments': 'Some comments',
|
||||
@@ -591,22 +657,41 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider_network,provider_account,status",
|
||||
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||
"cid,provider_network,provider_account,type,status",
|
||||
(
|
||||
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
(
|
||||
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
(
|
||||
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,cid,description,status",
|
||||
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
"id,cid,description,type,status",
|
||||
(
|
||||
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
(
|
||||
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
(
|
||||
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': virtual_circuit_types[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
@@ -620,6 +705,7 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
{{
|
||||
"cid": "Virtual Circuit 7",
|
||||
"provider_network": "Provider Network 1",
|
||||
"type": "Virtual Circuit Type 1",
|
||||
"status": "active",
|
||||
"terminations": [
|
||||
{{
|
||||
@@ -758,27 +844,35 @@ class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase)
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1'
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2'
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3'
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4'
|
||||
cid='Virtual Circuit 4',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
@@ -42,6 +42,9 @@ urlpatterns = [
|
||||
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
|
||||
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
|
||||
|
||||
path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))),
|
||||
path('virtual-circuit-types/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittype'))),
|
||||
|
||||
# Virtual circuit terminations
|
||||
path(
|
||||
'virtual-circuit-terminations/',
|
||||
|
||||
@@ -579,6 +579,67 @@ class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit Types
|
||||
#
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'list', path='', detail=False)
|
||||
class VirtualCircuitTypeListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
virtual_circuit_count=count_related(VirtualCircuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
filterset_form = forms.VirtualCircuitTypeFilterForm
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType)
|
||||
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'add', detail=False)
|
||||
@register_model_view(VirtualCircuitType, 'edit')
|
||||
class VirtualCircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
form = forms.VirtualCircuitTypeForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'delete')
|
||||
class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
|
||||
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
model_form = forms.VirtualCircuitTypeImportForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False)
|
||||
class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
form = forms.VirtualCircuitTypeBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
|
||||
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +21,11 @@ class CoreConfig(AppConfig):
|
||||
from core.api import schema # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
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()
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import requests
|
||||
import sys
|
||||
|
||||
from netbox.jobs import JobRunner
|
||||
from django.conf import settings
|
||||
from netbox.jobs import JobRunner, system_job
|
||||
from netbox.search.backends import search_backend
|
||||
from .choices import DataSourceStatusChoices
|
||||
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
||||
from .exceptions import SyncError
|
||||
from .models import DataSource
|
||||
|
||||
@@ -31,3 +34,44 @@ class SyncDataSourceJob(JobRunner):
|
||||
if type(e) is SyncError:
|
||||
logging.error(e)
|
||||
raise e
|
||||
|
||||
|
||||
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
||||
class SystemHousekeepingJob(JobRunner):
|
||||
"""
|
||||
Perform daily system housekeeping functions.
|
||||
"""
|
||||
class Meta:
|
||||
name = "System Housekeeping"
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
# Skip if running in development or test mode
|
||||
if settings.DEBUG or 'test' in sys.argv:
|
||||
return
|
||||
|
||||
# TODO: Migrate other housekeeping functions from the `housekeeping` management command.
|
||||
self.send_census_report()
|
||||
|
||||
@staticmethod
|
||||
def send_census_report():
|
||||
"""
|
||||
Send a census report (if enabled).
|
||||
"""
|
||||
# Skip if census reporting is disabled
|
||||
if settings.ISOLATED_DEPLOYMENT or not settings.CENSUS_REPORTING_ENABLED:
|
||||
return
|
||||
|
||||
census_data = {
|
||||
'version': settings.RELEASE.full_version,
|
||||
'python_version': sys.version.split()[0],
|
||||
'deployment_id': settings.DEPLOYMENT_ID,
|
||||
}
|
||||
try:
|
||||
requests.get(
|
||||
url=settings.CENSUS_URL,
|
||||
params=census_data,
|
||||
timeout=3,
|
||||
proxies=settings.HTTP_PROXIES
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
@@ -570,8 +570,9 @@ class SystemView(UserPassesTestMixin, View):
|
||||
return response
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
|
||||
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
|
||||
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
|
||||
if hasattr(config, attr) and getattr(config, attr, None):
|
||||
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
@@ -594,7 +595,7 @@ class BasePluginView(UserPassesTestMixin, View):
|
||||
catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False)
|
||||
if not catalog_plugins_error:
|
||||
catalog_plugins = get_catalog_plugins()
|
||||
if not catalog_plugins:
|
||||
if not catalog_plugins and not settings.ISOLATED_DEPLOYMENT:
|
||||
# Cache for 5 minutes to avoid spamming connection
|
||||
cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300)
|
||||
messages.warning(request, _("Plugins catalog could not be loaded"))
|
||||
|
||||
@@ -170,8 +170,8 @@ class MACAddressSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object',
|
||||
'description', 'comments',
|
||||
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id',
|
||||
'assigned_object', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'mac_address', 'description')
|
||||
|
||||
|
||||
@@ -362,6 +362,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,
|
||||
@@ -441,7 +446,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',
|
||||
|
||||
@@ -258,6 +258,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,
|
||||
@@ -267,8 +274,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,
|
||||
@@ -291,9 +303,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):
|
||||
@@ -305,6 +317,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(
|
||||
|
||||
@@ -3,9 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import MACAddress
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import DynamicModelChoiceField
|
||||
|
||||
__all__ = (
|
||||
'InterfaceCommonForm',
|
||||
@@ -20,12 +18,6 @@ class InterfaceCommonForm(forms.Form):
|
||||
max_value=INTERFACE_MTU_MAX,
|
||||
label=_('MTU')
|
||||
)
|
||||
primary_mac_address = DynamicModelChoiceField(
|
||||
queryset=MACAddress.objects.all(),
|
||||
label=_('Primary MAC address'),
|
||||
required=False,
|
||||
quick_add=True
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -1410,6 +1410,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
required=False,
|
||||
label=_('VRF')
|
||||
)
|
||||
primary_mac_address = DynamicModelChoiceField(
|
||||
queryset=MACAddress.objects.all(),
|
||||
label=_('Primary MAC address'),
|
||||
required=False,
|
||||
quick_add=True,
|
||||
quick_add_params={'interface': '$pk'}
|
||||
)
|
||||
wwn = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
|
||||
@@ -115,7 +115,7 @@ class ModularComponentTemplateType(ComponentTemplateType):
|
||||
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')],
|
||||
|
||||
@@ -605,6 +605,10 @@ class CablePath(models.Model):
|
||||
cable_end = 'A' if lct.cable_end == 'B' else 'B'
|
||||
q_filter |= Q(cable=lct.cable, cable_end=cable_end)
|
||||
|
||||
# Make sure this filter has been populated; if not, we have probably been given invalid data
|
||||
if not q_filter:
|
||||
break
|
||||
|
||||
remote_cable_terminations = CableTermination.objects.filter(q_filter)
|
||||
remote_terminations = [ct.termination for ct in remote_cable_terminations]
|
||||
else:
|
||||
|
||||
@@ -1277,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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||
|
||||
__all__ = (
|
||||
@@ -84,6 +86,16 @@ class CachedScopeMixin(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def clean(self):
|
||||
if self.scope_type and not self.scope:
|
||||
scope_type = self.scope_type.model_class()
|
||||
raise ValidationError({
|
||||
'scope': _(
|
||||
"Please select a {scope_type}."
|
||||
).format(scope_type=scope_type._meta.model_name)
|
||||
})
|
||||
super().clean()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
self.cache_related_objects()
|
||||
|
||||
@@ -374,22 +374,27 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
if not self._state.adding:
|
||||
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
|
||||
|
||||
effective_u_height = self.rack_type.u_height if self.rack_type else self.u_height
|
||||
effective_starting_unit = self.rack_type.starting_unit if self.rack_type else self.starting_unit
|
||||
|
||||
# Validate that Rack is tall enough to house the highest mounted Device
|
||||
if top_device := mounted_devices.last():
|
||||
min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
|
||||
if self.u_height < min_height:
|
||||
min_height = top_device.position + top_device.device_type.u_height - effective_starting_unit
|
||||
if effective_u_height < min_height:
|
||||
field = 'rack_type' if self.rack_type else 'u_height'
|
||||
raise ValidationError({
|
||||
'u_height': _(
|
||||
field: _(
|
||||
"Rack must be at least {min_height}U tall to house currently installed devices."
|
||||
).format(min_height=min_height)
|
||||
})
|
||||
|
||||
# Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
|
||||
if last_device := mounted_devices.first():
|
||||
if self.starting_unit > last_device.position:
|
||||
if effective_starting_unit > last_device.position:
|
||||
field = 'rack_type' if self.rack_type else 'starting_unit'
|
||||
raise ValidationError({
|
||||
'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
|
||||
"currently installed devices.").format(position=last_device.position)
|
||||
field: _("Rack unit numbering must begin at {position} or less to house "
|
||||
"currently installed devices.").format(position=last_device.position)
|
||||
})
|
||||
|
||||
# Validate that Rack was assigned a Location of its same site, if applicable
|
||||
|
||||
@@ -105,7 +105,7 @@ class MACAddressIndex(SearchIndex):
|
||||
('mac_address', 100),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('mac_address', 'interface')
|
||||
display_attrs = ('assigned_object', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -85,7 +85,8 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
||||
if instance._terminations_modified:
|
||||
a_terminations = []
|
||||
b_terminations = []
|
||||
for t in instance.terminations.all():
|
||||
# Note: instance.terminations.all() is not safe to use here as it might be stale
|
||||
for t in CableTermination.objects.filter(cable=instance):
|
||||
if t.cable_end == CableEndChoices.SIDE_A:
|
||||
a_terminations.append(t.termination)
|
||||
else:
|
||||
|
||||
@@ -362,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:
|
||||
|
||||
@@ -1155,6 +1155,7 @@ class MACAddressTable(NetBoxTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.MACAddress
|
||||
fields = (
|
||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'created', 'last_updated',
|
||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object')
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description')
|
||||
|
||||
@@ -41,6 +41,7 @@ class ModuleTypeTable(NetBoxTable):
|
||||
model = ModuleType
|
||||
fields = (
|
||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'comments', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'model', 'manufacturer', 'part_number',
|
||||
@@ -79,7 +80,7 @@ class ModuleTable(NetBoxTable):
|
||||
model = Module
|
||||
fields = (
|
||||
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
|
||||
'description', 'comments', 'tags',
|
||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
|
||||
|
||||
@@ -69,16 +69,18 @@ INTERFACE_FHRPGROUPS = """
|
||||
"""
|
||||
|
||||
INTERFACE_TAGGED_VLANS = """
|
||||
{% if record.mode == 'tagged' %}
|
||||
{% load i18n %}
|
||||
{% if record.mode == 'access' %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
{% 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 />
|
||||
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
All
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -2447,3 +2447,46 @@ class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
|
||||
# Omit identifier to test uniqueness constraint
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class MACAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
model = MACAddress
|
||||
brief_fields = ['description', 'display', 'id', 'mac_address', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device(name='Device 1')
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 2', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 3', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 4', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 5', type='1000base-t'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
mac_addresses = (
|
||||
MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
|
||||
MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
|
||||
MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
|
||||
)
|
||||
MACAddress.objects.bulk_create(mac_addresses)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'mac_address': '00:00:00:00:00:04',
|
||||
'assigned_object_type': 'dcim.interface',
|
||||
'assigned_object_id': interfaces[3].pk,
|
||||
},
|
||||
{
|
||||
'mac_address': '00:00:00:00:00:05',
|
||||
'assigned_object_type': 'dcim.interface',
|
||||
'assigned_object_id': interfaces[4].pk,
|
||||
},
|
||||
{
|
||||
'mac_address': '00:00:00:00:00:06',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3470,3 +3470,54 @@ class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.bulk_edit_data = {
|
||||
'status': VirtualDeviceContextStatusChoices.STATUS_OFFLINE,
|
||||
}
|
||||
|
||||
|
||||
class MACAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = MACAddress
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device(name='Device 1')
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 2', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 3', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 4', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 5', type='1000base-t'),
|
||||
Interface(device=device, name='Interface 6', type='1000base-t'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
mac_addresses = (
|
||||
MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
|
||||
MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
|
||||
MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
|
||||
)
|
||||
MACAddress.objects.bulk_create(mac_addresses)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'mac_address': EUI('00:00:00:00:00:04'),
|
||||
'description': 'New MAC address',
|
||||
'interface_id': interfaces[3].pk,
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"mac_address,device,interface",
|
||||
"00:00:00:00:00:04,Device 1,Interface 4",
|
||||
"00:00:00:00:00:05,Device 1,Interface 5",
|
||||
"00:00:00:00:00:06,Device 1,Interface 6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,mac_address",
|
||||
f"{mac_addresses[0].pk},00:00:00:00:00:0a",
|
||||
f"{mac_addresses[1].pk},00:00:00:00:00:0b",
|
||||
f"{mac_addresses[2].pk},00:00:00:00:00:0c",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -17,7 +15,7 @@ from jinja2.exceptions import TemplateError
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import ASN, IPAddress, VLANGroup
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
@@ -32,8 +30,9 @@ from utilities.views import (
|
||||
)
|
||||
from virtualization.filtersets import VirtualMachineFilterSet
|
||||
from virtualization.forms import VirtualMachineFilterForm
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from virtualization.tables import VirtualMachineTable
|
||||
from wireless.models import WirelessLAN
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
@@ -240,6 +239,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
regions,
|
||||
omit=(Cluster, Prefix, WirelessLAN),
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
@@ -249,6 +249,11 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
).distinct(),
|
||||
'region_id'
|
||||
),
|
||||
|
||||
# Handle these relations manually to avoid erroneous filter name resolution
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(_region__in=regions), 'region_id'),
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(_region__in=regions), 'region_id'),
|
||||
(WirelessLAN.objects.restrict(request.user, 'view').filter(_region__in=regions), 'region_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -333,6 +338,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
groups,
|
||||
omit=(Cluster, Prefix, WirelessLAN),
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
@@ -342,6 +348,20 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
).distinct(),
|
||||
'site_group_id'
|
||||
),
|
||||
|
||||
# Handle these relations manually to avoid erroneous filter name resolution
|
||||
(
|
||||
Cluster.objects.restrict(request.user, 'view').filter(_site_group__in=groups),
|
||||
'site_group_id'
|
||||
),
|
||||
(
|
||||
Prefix.objects.restrict(request.user, 'view').filter(_site_group__in=groups),
|
||||
'site_group_id'
|
||||
),
|
||||
(
|
||||
WirelessLAN.objects.restrict(request.user, 'view').filter(_site_group__in=groups),
|
||||
'site_group_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -420,8 +440,8 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
[CableTermination, CircuitTermination],
|
||||
(
|
||||
omit=(CableTermination, CircuitTermination, Cluster, Prefix, WirelessLAN),
|
||||
extra=(
|
||||
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
@@ -431,6 +451,11 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___site=instance).distinct(),
|
||||
'site_id'
|
||||
),
|
||||
|
||||
# Handle these relations manually to avoid erroneous filter name resolution
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(_site=instance), 'site_id'),
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(_site=instance), 'site_id'),
|
||||
(WirelessLAN.objects.restrict(request.user, 'view').filter(_site=instance), 'site_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -508,14 +533,19 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
locations,
|
||||
[CableTermination],
|
||||
(
|
||||
omit=[CableTermination, Cluster, Prefix, WirelessLAN],
|
||||
extra=(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(
|
||||
terminations___location=instance
|
||||
).distinct(),
|
||||
'location_id'
|
||||
),
|
||||
|
||||
# Handle these relations manually to avoid erroneous filter name resolution
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
|
||||
(WirelessLAN.objects.restrict(request.user, 'view').filter(_location=instance), 'location_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -2238,7 +2268,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
|
||||
@@ -2256,17 +2287,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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ DEFAULT_DASHBOARD = [
|
||||
'feed_url': 'http://netbox.dev/rss/',
|
||||
'max_entries': 10,
|
||||
'cache_timeout': 14400,
|
||||
'requires_internet': True,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -275,6 +275,7 @@ class RSSFeedWidget(DashboardWidget):
|
||||
default_config = {
|
||||
'max_entries': 10,
|
||||
'cache_timeout': 3600, # seconds
|
||||
'requires_internet': True,
|
||||
}
|
||||
description = _('Embed an RSS feed from an external website.')
|
||||
template_name = 'extras/dashboard/widgets/rssfeed.html'
|
||||
@@ -285,6 +286,10 @@ class RSSFeedWidget(DashboardWidget):
|
||||
feed_url = forms.URLField(
|
||||
label=_('Feed URL')
|
||||
)
|
||||
requires_internet = forms.BooleanField(
|
||||
label=_('Requires external connection'),
|
||||
required=False,
|
||||
)
|
||||
max_entries = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=1000,
|
||||
@@ -309,6 +314,11 @@ class RSSFeedWidget(DashboardWidget):
|
||||
return f'dashboard_rss_{url_checksum}'
|
||||
|
||||
def get_feed(self):
|
||||
if self.config.get('requires_internet') and settings.ISOLATED_DEPLOYMENT:
|
||||
return {
|
||||
'isolated_deployment': True,
|
||||
}
|
||||
|
||||
# Fetch RSS content from cache if available
|
||||
if feed_content := cache.get(self.cache_key):
|
||||
return {
|
||||
|
||||
@@ -90,6 +90,10 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
if not event_rule.eval_conditions(data):
|
||||
continue
|
||||
|
||||
# Compile event data
|
||||
event_data = event_rule.action_data or {}
|
||||
event_data.update(data)
|
||||
|
||||
# Webhooks
|
||||
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
||||
|
||||
@@ -102,7 +106,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
"event_rule": event_rule,
|
||||
"model_name": object_type.model,
|
||||
"event_type": event_type,
|
||||
"data": data,
|
||||
"data": event_data,
|
||||
"snapshots": snapshots,
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"username": username,
|
||||
@@ -130,7 +134,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
instance=event_rule.action_object,
|
||||
name=script.name,
|
||||
user=user,
|
||||
data=data
|
||||
data=event_data
|
||||
)
|
||||
|
||||
# Notification groups
|
||||
@@ -138,8 +142,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
# Bulk-create notifications for all members of the notification group
|
||||
event_rule.action_object.notify(
|
||||
object_type=object_type,
|
||||
object_id=data['id'],
|
||||
object_repr=data.get('display'),
|
||||
object_id=event_data['id'],
|
||||
object_repr=event_data.get('display'),
|
||||
event_type=event_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
|
||||
|
||||
@@ -100,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)
|
||||
|
||||
@@ -120,11 +120,12 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Apply Location & DeviceType filters only for VirtualMachines
|
||||
if self.model._meta.model_name == 'device':
|
||||
base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
|
||||
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
|
||||
|
||||
elif self.model._meta.model_name == 'virtualmachine':
|
||||
base_query.add(Q(locations=None), Q.AND)
|
||||
base_query.add(Q(device_types=None), Q.AND)
|
||||
|
||||
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
|
||||
|
||||
@@ -50,21 +50,24 @@ class EventRuleTest(APITestCase):
|
||||
event_types=[OBJECT_CREATED],
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
action_object_id=webhooks[0].id,
|
||||
action_data={"foo": 1},
|
||||
),
|
||||
EventRule(
|
||||
name='Event Rule 2',
|
||||
event_types=[OBJECT_UPDATED],
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
action_object_id=webhooks[0].id,
|
||||
action_data={"foo": 2},
|
||||
),
|
||||
EventRule(
|
||||
name='Event Rule 3',
|
||||
event_types=[OBJECT_DELETED],
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
action_object_id=webhooks[0].id,
|
||||
action_data={"foo": 3},
|
||||
),
|
||||
))
|
||||
for event_rule in event_rules:
|
||||
@@ -134,6 +137,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], response.data['id'])
|
||||
self.assertEqual(job.kwargs['data']['foo'], 1)
|
||||
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data['tags']))
|
||||
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], 'Site 1')
|
||||
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Bar', 'Foo'])
|
||||
@@ -184,6 +188,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], response.data[i]['id'])
|
||||
self.assertEqual(job.kwargs['data']['foo'], 1)
|
||||
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data[i]['tags']))
|
||||
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], response.data[i]['name'])
|
||||
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Bar', 'Foo'])
|
||||
@@ -215,6 +220,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], site.pk)
|
||||
self.assertEqual(job.kwargs['data']['foo'], 2)
|
||||
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data['tags']))
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], 'Site 1')
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
|
||||
@@ -271,6 +277,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], data[i]['id'])
|
||||
self.assertEqual(job.kwargs['data']['foo'], 2)
|
||||
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data[i]['tags']))
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], sites[i].name)
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
|
||||
@@ -297,6 +304,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], site.pk)
|
||||
self.assertEqual(job.kwargs['data']['foo'], 3)
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], 'Site 1')
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
|
||||
|
||||
@@ -330,6 +338,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['data']['id'], sites[i].pk)
|
||||
self.assertEqual(job.kwargs['data']['foo'], 3)
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], sites[i].name)
|
||||
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
|
||||
|
||||
@@ -358,6 +367,7 @@ class EventRuleTest(APITestCase):
|
||||
self.assertEqual(body['username'], 'testuser')
|
||||
self.assertEqual(body['request_id'], str(request_id))
|
||||
self.assertEqual(body['data']['name'], 'Site 1')
|
||||
self.assertEqual(body['data']['foo'], 1)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -1170,6 +1170,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'virtualchassis',
|
||||
'virtualcircuit',
|
||||
'virtualcircuittermination',
|
||||
'virtualcircuittype',
|
||||
'virtualdevicecontext',
|
||||
'virtualdisk',
|
||||
'virtualmachine',
|
||||
|
||||
@@ -1210,12 +1210,14 @@ class ScriptView(BaseScriptView):
|
||||
script_class = self._get_script_class(script)
|
||||
if not script_class:
|
||||
return render(request, 'extras/script.html', {
|
||||
'object': script,
|
||||
'script': script,
|
||||
})
|
||||
|
||||
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
||||
|
||||
return render(request, 'extras/script.html', {
|
||||
'object': script,
|
||||
'script': script,
|
||||
'script_class': script_class,
|
||||
'form': form,
|
||||
@@ -1231,6 +1233,7 @@ class ScriptView(BaseScriptView):
|
||||
script_class = self._get_script_class(script)
|
||||
if not script_class:
|
||||
return render(request, 'extras/script.html', {
|
||||
'object': script,
|
||||
'script': script,
|
||||
})
|
||||
|
||||
@@ -1255,6 +1258,7 @@ class ScriptView(BaseScriptView):
|
||||
return redirect('extras:script_result', job_pk=job.pk)
|
||||
|
||||
return render(request, 'extras/script.html', {
|
||||
'object': script,
|
||||
'script': script,
|
||||
'script_class': script.python_class(),
|
||||
'form': form,
|
||||
|
||||
@@ -214,8 +214,10 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
@@ -1056,7 +1058,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
return queryset.filter(
|
||||
Q(interfaces_as_tagged=value) |
|
||||
Q(interfaces_as_untagged=value)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
def filter_vminterface_id(self, queryset, name, value):
|
||||
if value is None:
|
||||
@@ -1064,7 +1066,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
return queryset.filter(
|
||||
Q(vminterfaces_as_tagged=value) |
|
||||
Q(vminterfaces_as_untagged=value)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
@@ -325,12 +325,17 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
help_text=_('Make this the primary IP for the assigned device'),
|
||||
required=False
|
||||
)
|
||||
is_oob = forms.BooleanField(
|
||||
label=_('Is out-of-band'),
|
||||
help_text=_('Designate this as the out-of-band IP address for the assigned device'),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'dns_name', 'description', 'comments', 'tags',
|
||||
'is_oob', 'dns_name', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@@ -344,7 +349,7 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
**{f"device__{self.fields['device'].to_field_name}": data['device']}
|
||||
)
|
||||
|
||||
# Limit interface queryset by assigned device
|
||||
# Limit interface queryset by assigned VM
|
||||
elif data.get('virtual_machine'):
|
||||
self.fields['interface'].queryset = VMInterface.objects.filter(
|
||||
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
|
||||
@@ -357,16 +362,29 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
virtual_machine = self.cleaned_data.get('virtual_machine')
|
||||
interface = self.cleaned_data.get('interface')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
is_oob = self.cleaned_data.get('is_oob')
|
||||
|
||||
# Validate is_primary
|
||||
# Validate is_primary and is_oob
|
||||
if is_primary and not device and not virtual_machine:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": _("No device or virtual machine specified; cannot set as primary IP")
|
||||
})
|
||||
if is_oob and not device:
|
||||
raise forms.ValidationError({
|
||||
"is_oob": _("No device specified; cannot set as out-of-band IP")
|
||||
})
|
||||
if is_oob and virtual_machine:
|
||||
raise forms.ValidationError({
|
||||
"is_oob": _("Cannot set out-of-band IP for virtual machines")
|
||||
})
|
||||
if is_primary and not interface:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": _("No interface specified; cannot set as primary IP")
|
||||
})
|
||||
if is_oob and not interface:
|
||||
raise forms.ValidationError({
|
||||
"is_oob": _("No interface specified; cannot set as out-of-band IP")
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -385,6 +403,12 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
parent.primary_ip6 = ipaddress
|
||||
parent.save()
|
||||
|
||||
# Set as OOB for device
|
||||
if self.cleaned_data.get('is_oob'):
|
||||
parent = self.cleaned_data.get('device')
|
||||
parent.oob_ip = ipaddress
|
||||
parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
||||
|
||||
|
||||
@@ -311,6 +311,10 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
required=False,
|
||||
label=_('Make this the primary IP for the device/VM')
|
||||
)
|
||||
oob_for_parent = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Make this the out-of-band IP for the device')
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
@@ -322,7 +326,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
FieldSet('vminterface', name=_('Virtual Machine')),
|
||||
FieldSet('fhrpgroup', name=_('FHRP Group')),
|
||||
),
|
||||
'primary_for_parent', name=_('Assignment')
|
||||
'primary_for_parent', 'oob_for_parent', name=_('Assignment')
|
||||
),
|
||||
FieldSet('nat_inside', name=_('NAT IP (Inside)')),
|
||||
)
|
||||
@@ -330,8 +334,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_inside', 'tenant_group',
|
||||
'tenant', 'description', 'comments', 'tags',
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
|
||||
'tenant_group', 'tenant', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -350,7 +354,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize primary_for_parent if IP address is already assigned
|
||||
# Initialize parent object & fields if IP address is already assigned
|
||||
if self.instance.pk and self.instance.assigned_object:
|
||||
parent = getattr(self.instance.assigned_object, 'parent_object', None)
|
||||
if parent and (
|
||||
@@ -359,6 +363,9 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
):
|
||||
self.initial['primary_for_parent'] = True
|
||||
|
||||
if parent and getattr(parent, 'oob_ip_id', None) == self.instance.pk:
|
||||
self.initial['oob_for_parent'] = True
|
||||
|
||||
if type(instance.assigned_object) is Interface:
|
||||
self.fields['interface'].widget.add_query_params({
|
||||
'device_id': instance.assigned_object.device.pk,
|
||||
@@ -387,15 +394,15 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
})
|
||||
elif selected_objects:
|
||||
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||
if (
|
||||
self.instance.pk and
|
||||
self.instance.assigned_object and
|
||||
self.cleaned_data['primary_for_parent'] and
|
||||
assigned_object != self.instance.assigned_object
|
||||
):
|
||||
raise ValidationError(
|
||||
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
|
||||
)
|
||||
if self.instance.pk and self.instance.assigned_object and assigned_object != self.instance.assigned_object:
|
||||
if self.cleaned_data['primary_for_parent']:
|
||||
raise ValidationError(
|
||||
_("Cannot reassign primary IP address for the parent device/VM")
|
||||
)
|
||||
if self.cleaned_data['oob_for_parent']:
|
||||
raise ValidationError(
|
||||
_("Cannot reassign out-of-Band IP address for the parent device")
|
||||
)
|
||||
self.instance.assigned_object = assigned_object
|
||||
else:
|
||||
self.instance.assigned_object = None
|
||||
@@ -407,6 +414,16 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
|
||||
)
|
||||
|
||||
# OOB IP assignment is only available if device interface has been assigned.
|
||||
interface = self.cleaned_data.get('interface')
|
||||
if self.cleaned_data.get('oob_for_parent') and not interface:
|
||||
self.add_error(
|
||||
'oob_for_parent', _(
|
||||
"Only IP addresses assigned to a device interface can be designated as the out-of-band IP for a "
|
||||
"device."
|
||||
)
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
@@ -428,6 +445,17 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
parent.primary_ip6 = None
|
||||
parent.save()
|
||||
|
||||
# Assign/clear this IPAddress as the OOB for the associated Device
|
||||
if type(interface) is Interface:
|
||||
parent = interface.parent_object
|
||||
parent.snapshot()
|
||||
if self.cleaned_data['oob_for_parent']:
|
||||
parent.oob_ip = ipaddress
|
||||
parent.save()
|
||||
elif parent.oob_ip == ipaddress:
|
||||
parent.oob_ip = None
|
||||
parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class PrefixIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
display_attrs = ('scope', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -192,7 +192,8 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||
)
|
||||
scope = tables.Column(
|
||||
verbose_name=_('Scope'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
vlan_group = tables.Column(
|
||||
accessor='vlan__group',
|
||||
@@ -200,6 +201,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name=_('VLAN Group')
|
||||
)
|
||||
vlan = tables.Column(
|
||||
order_by=('vlan__vid', 'vlan__pk'),
|
||||
linkify=True,
|
||||
verbose_name=_('VLAN')
|
||||
)
|
||||
|
||||
@@ -713,7 +713,7 @@ class IPRangeView(generic.ObjectView):
|
||||
Q(prefix__net_contains_or_equals=str(instance.end_address.ip)),
|
||||
vrf=instance.vrf
|
||||
).prefetch_related(
|
||||
'site', 'role', 'tenant', 'vlan', 'role'
|
||||
'scope', 'role', 'tenant', 'vlan', 'role'
|
||||
)
|
||||
parent_prefixes_table = tables.PrefixTable(
|
||||
list(parent_prefixes),
|
||||
@@ -805,7 +805,7 @@ class IPAddressView(generic.ObjectView):
|
||||
vrf=instance.vrf,
|
||||
prefix__net_contains_or_equals=str(instance.address.ip)
|
||||
).prefetch_related(
|
||||
'site', 'role'
|
||||
'scope', 'role'
|
||||
)
|
||||
parent_prefixes_table = tables.PrefixTable(
|
||||
list(parent_prefixes),
|
||||
@@ -1288,7 +1288,7 @@ class VLANView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
|
||||
'vrf', 'site', 'role', 'tenant'
|
||||
'vrf', 'scope', 'role', 'tenant'
|
||||
)
|
||||
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
|
||||
|
||||
|
||||
@@ -38,12 +38,14 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
|
||||
def get_limit(self, request):
|
||||
if self.limit_query_param:
|
||||
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
|
||||
if MAX_PAGE_SIZE:
|
||||
MAX_PAGE_SIZE = max(MAX_PAGE_SIZE, self.default_limit)
|
||||
try:
|
||||
limit = int(request.query_params[self.limit_query_param])
|
||||
if limit < 0:
|
||||
raise ValueError()
|
||||
# Enforce maximum page size, if defined
|
||||
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
|
||||
if MAX_PAGE_SIZE:
|
||||
return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
|
||||
return limit
|
||||
|
||||
@@ -76,6 +76,12 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
||||
Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
|
||||
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
|
||||
"""
|
||||
|
||||
# Bypass DRF's built-in validation of unique constraints due to DRF bug #9410. Rely instead
|
||||
# on our own custom model validation (below).
|
||||
def get_unique_together_constraints(self, model):
|
||||
return []
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Skip validation if we're being used to represent a nested object
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.utils import register_request_processor
|
||||
from extras.events import flush_events
|
||||
|
||||
|
||||
@register_request_processor
|
||||
@contextmanager
|
||||
def event_tracking(request):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from contextlib import ExitStack
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
@@ -10,7 +12,7 @@ from django.db.utils import InternalError
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
|
||||
from netbox.config import clear_config, get_config
|
||||
from netbox.context_managers import event_tracking
|
||||
from netbox.registry import registry
|
||||
from netbox.views import handler_500
|
||||
from utilities.api import is_api_request
|
||||
from utilities.error_handlers import handle_rest_api_exception
|
||||
@@ -32,8 +34,10 @@ class CoreMiddleware:
|
||||
# Assign a random unique ID to the request. This will be used for change logging.
|
||||
request.id = uuid.uuid4()
|
||||
|
||||
# Enable the event_tracking context manager and process the request.
|
||||
with event_tracking(request):
|
||||
# Apply all registered request processors
|
||||
with ExitStack() as stack:
|
||||
for request_processor in registry['request_processors']:
|
||||
stack.enter_context(request_processor(request))
|
||||
response = self.get_response(request)
|
||||
|
||||
# Check if language cookie should be renewed
|
||||
|
||||
@@ -279,8 +279,6 @@ CIRCUITS_MENU = Menu(
|
||||
items=(
|
||||
get_model_item('circuits', 'circuit', _('Circuits')),
|
||||
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
||||
get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
|
||||
get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
|
||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
@@ -288,9 +286,17 @@ CIRCUITS_MENU = Menu(
|
||||
label=_('Virtual Circuits'),
|
||||
items=(
|
||||
get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')),
|
||||
get_model_item('circuits', 'virtualcircuittype', _('Virtual Circuit Types')),
|
||||
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Groups'),
|
||||
items=(
|
||||
get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
|
||||
get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
label=_('Providers'),
|
||||
items=(
|
||||
|
||||
@@ -29,6 +29,7 @@ registry = Registry({
|
||||
'model_features': dict(),
|
||||
'models': collections.defaultdict(set),
|
||||
'plugins': dict(),
|
||||
'request_processors': list(),
|
||||
'search': dict(),
|
||||
'system_jobs': dict(),
|
||||
'tables': collections.defaultdict(dict),
|
||||
|
||||
@@ -5,9 +5,7 @@ import os
|
||||
import platform
|
||||
import sys
|
||||
import warnings
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
@@ -224,8 +222,18 @@ DATABASES = {
|
||||
# Storage backend
|
||||
#
|
||||
|
||||
# Default STORAGES for Django
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
if STORAGE_BACKEND is not None:
|
||||
DEFAULT_FILE_STORAGE = STORAGE_BACKEND
|
||||
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
|
||||
|
||||
# django-storages
|
||||
if STORAGE_BACKEND.startswith('storages.'):
|
||||
@@ -583,17 +591,6 @@ if SENTRY_ENABLED:
|
||||
# Calculate a unique deployment ID from the secret key
|
||||
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
|
||||
CENSUS_URL = 'https://census.netbox.oss.netboxlabs.com/api/v1/'
|
||||
CENSUS_PARAMS = {
|
||||
'version': RELEASE.full_version,
|
||||
'python_version': sys.version.split()[0],
|
||||
'deployment_id': DEPLOYMENT_ID,
|
||||
}
|
||||
if CENSUS_REPORTING_ENABLED and not ISOLATED_DEPLOYMENT and not DEBUG and 'test' not in sys.argv:
|
||||
try:
|
||||
# Report anonymous census data
|
||||
requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -3,6 +3,7 @@ from netbox.registry import registry
|
||||
__all__ = (
|
||||
'get_data_backend_choices',
|
||||
'register_data_backend',
|
||||
'register_request_processor',
|
||||
)
|
||||
|
||||
|
||||
@@ -24,3 +25,12 @@ def register_data_backend():
|
||||
return cls
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
def register_request_processor(func):
|
||||
"""
|
||||
Decorator for registering a request processor.
|
||||
"""
|
||||
registry['request_processors'].append(func)
|
||||
|
||||
return func
|
||||
|
||||
@@ -661,15 +661,13 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
elif 'virtual_machine' in request.GET:
|
||||
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
|
||||
|
||||
if '_apply' in request.POST:
|
||||
form = self.form(request.POST, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
form = self.form(request.POST, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
if '_apply' in request.POST:
|
||||
if form.is_valid():
|
||||
logger.debug("Form validation was successful")
|
||||
|
||||
try:
|
||||
|
||||
with transaction.atomic():
|
||||
updated_objects = self._update_objects(form, request)
|
||||
|
||||
@@ -697,10 +695,6 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
else:
|
||||
logger.debug("Form validation failed")
|
||||
|
||||
else:
|
||||
form = self.form(initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
# Retrieve objects being edited
|
||||
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
|
||||
if not table.rows:
|
||||
@@ -744,7 +738,6 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
renamed_pks = []
|
||||
|
||||
for obj in selected_objects:
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
@@ -758,7 +751,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
except re.error:
|
||||
obj.new_name = obj.name
|
||||
else:
|
||||
obj.new_name = obj.name.replace(find, replace)
|
||||
obj.new_name = (obj.name or '').replace(find, replace)
|
||||
renamed_pks.append(obj.pk)
|
||||
|
||||
return renamed_pks
|
||||
@@ -793,6 +786,10 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
except IntegrityError as e:
|
||||
messages.error(self.request, ", ".join(e.args))
|
||||
clear_events.send(sender=self)
|
||||
|
||||
except (AbortRequest, PermissionsViolation) as e:
|
||||
logger.debug(e.message)
|
||||
form.add_error(None, e.message)
|
||||
|
||||
45260
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
45260
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user