1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'feature' into develop

This commit is contained in:
jeremystretch
2022-04-05 14:51:26 -04:00
496 changed files with 20619 additions and 11509 deletions

View File

@ -3,10 +3,12 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
NETBOX_CONFIGURATION: netbox.configuration_testing
strategy: strategy:
matrix: matrix:
python-version: [3.7, 3.8, 3.9] python-version: ['3.8', '3.9', '3.10']
node-version: [14.x] node-version: ['14.x']
services: services:
redis: redis:
image: redis image: redis
@ -57,7 +59,6 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install pycodestyle coverage tblib pip install pycodestyle coverage tblib
ln -s configuration.testing.py netbox/netbox/configuration.py
- name: Build documentation - name: Build documentation
run: mkdocs build run: mkdocs build

10
.readthedocs.yaml Normal file
View File

@ -0,0 +1,10 @@
version: 2
build:
os: ubuntu-20.04
tools:
python: "3.9"
mkdocs:
configuration: mkdocs.yml
python:
install:
- requirements: requirements.txt

View File

@ -1,6 +1,6 @@
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://github.com/django/django # https://github.com/django/django
Django<4.0 Django
# Django middleware which permits cross-domain API requests # Django middleware which permits cross-domain API requests
# https://github.com/OttoYiu/django-cors-headers # https://github.com/OttoYiu/django-cors-headers
@ -82,6 +82,10 @@ markdown-include
# https://github.com/squidfunk/mkdocs-material # https://github.com/squidfunk/mkdocs-material
mkdocs-material mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings
mkdocstrings
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses
# https://github.com/netaddr/netaddr # https://github.com/netaddr/netaddr
netaddr netaddr
@ -113,3 +117,7 @@ svgwrite
# Tabular dataset library (for table-based exports) # Tabular dataset library (for table-based exports)
# https://github.com/jazzband/tablib # https://github.com/jazzband/tablib
tablib tablib
# Timezone data (required by django-timezone-field on Python 3.9+)
# https://github.com/python/tzdata
tzdata

View File

@ -66,6 +66,22 @@ CUSTOM_VALIDATORS = {
--- ---
## DEFAULT_USER_PREFERENCES
This is a dictionary defining the default preferences to be set for newly-created user accounts. For example, to set the default page size for all users to 100, define the following:
```python
DEFAULT_USER_PREFERENCES = {
"pagination": {
"per_page": 100
}
}
```
For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`.
---
## ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
Default: False Default: False

View File

@ -1,6 +1,11 @@
# NetBox Configuration # NetBox Configuration
NetBox's local configuration is stored in `$INSTALL_ROOT/netbox/netbox/configuration.py`. An example configuration is provided as `configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file. While NetBox has many configuration settings, only a few of them must be defined at the time of installation: these are defined under "required settings" below. NetBox's local configuration is stored in `$INSTALL_ROOT/netbox/netbox/configuration.py` by default. An example configuration is provided as `configuration_example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file. While NetBox has many configuration settings, only a few of them must be defined at the time of installation: these are defined under "required settings" below.
!!! info "Customizing the Configuration Module"
A custom configuration module may be specified by setting the `NETBOX_CONFIGURATION` environment variable. This must be a dotted path to the desired Python module. For example, a file named `my_config.py` in the same directory as `settings.py` would be referenced as `netbox.my_config`.
For the sake of brevity, the NetBox documentation refers to the configuration file simply as `configuration.py`.
Some configuration parameters may alternatively be defined either in `configuration.py` or within the administrative section of the user interface. Settings which are "hard-coded" in the configuration file take precedence over those defined via the UI. Some configuration parameters may alternatively be defined either in `configuration.py` or within the administrative section of the user interface. Settings which are "hard-coded" in the configuration file take precedence over those defined via the UI.

View File

@ -13,6 +13,23 @@ ADMINS = [
--- ---
## AUTH_PASSWORD_VALIDATORS
This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
```python
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 10,
}
},
]
```
---
## BASE_PATH ## BASE_PATH
Default: None Default: None
@ -49,6 +66,21 @@ CORS_ORIGIN_WHITELIST = [
--- ---
## CSRF_TRUSTED_ORIGINS
Default: `[]`
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
```python
CSRF_TRUSTED_ORIGINS = (
'http://netbox.local',
'https://netbox.local',
)
```
---
## DEBUG ## DEBUG
Default: False Default: False
@ -140,6 +172,66 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
--- ---
## FIELD_CHOICES
Some static choice fields on models can be configured with custom values. This is done by defining `FIELD_CHOICES` as a dictionary mapping model fields to their choices. Each choice in the list must have a database value and a human-friendly label, and may optionally specify a color. (A list of available colors is provided below.)
The choices provided can either replace the stock choices provided by NetBox, or append to them. To _replace_ the available choices, specify the app, model, and field name separated by dots. For example, the site model would be referenced as `dcim.Site.status`. To _extend_ the available choices, append a plus sign to the end of this string (e.g. `dcim.Site.status+`).
For example, the following configuration would replace the default site status choices with the options Foo, Bar, and Baz:
```python
FIELD_CHOICES = {
'dcim.Site.status': (
('foo', 'Foo', 'red'),
('bar', 'Bar', 'green'),
('baz', 'Baz', 'blue'),
)
}
```
Appending a plus sign to the field identifier would instead _add_ these choices to the ones already offered:
```python
FIELD_CHOICES = {
'dcim.Site.status+': (
...
)
}
```
The following model fields support configurable choices:
* `circuits.Circuit.status`
* `dcim.Device.status`
* `dcim.PowerFeed.status`
* `dcim.Rack.status`
* `dcim.Site.status`
* `extras.JournalEntry.kind`
* `ipam.IPAddress.status`
* `ipam.IPRange.status`
* `ipam.Prefix.status`
* `ipam.VLAN.status`
* `virtualization.VirtualMachine.status`
The following colors are supported:
* `blue`
* `indigo`
* `purple`
* `pink`
* `red`
* `orange`
* `yellow`
* `green`
* `teal`
* `cyan`
* `gray`
* `black`
* `white`
---
## HTTP_PROXIES ## HTTP_PROXIES
Default: None Default: None

View File

@ -37,4 +37,5 @@ Once component templates have been created, every new device that you create as
{!models/dcim/interfacetemplate.md!} {!models/dcim/interfacetemplate.md!}
{!models/dcim/frontporttemplate.md!} {!models/dcim/frontporttemplate.md!}
{!models/dcim/rearporttemplate.md!} {!models/dcim/rearporttemplate.md!}
{!models/dcim/modulebaytemplate.md!}
{!models/dcim/devicebaytemplate.md!} {!models/dcim/devicebaytemplate.md!}

View File

@ -17,6 +17,7 @@ Device components represent discrete objects within a device which are used to t
{!models/dcim/interface.md!} {!models/dcim/interface.md!}
{!models/dcim/frontport.md!} {!models/dcim/frontport.md!}
{!models/dcim/rearport.md!} {!models/dcim/rearport.md!}
{!models/dcim/modulebay.md!}
{!models/dcim/devicebay.md!} {!models/dcim/devicebay.md!}
{!models/dcim/inventoryitem.md!} {!models/dcim/inventoryitem.md!}

View File

@ -0,0 +1,4 @@
# Modules
{!models/dcim/moduletype.md!}
{!models/dcim/module.md!}

View File

@ -1,3 +1,4 @@
# Service Mapping # Service Mapping
{!models/ipam/servicetemplate.md!}
{!models/ipam/service.md!} {!models/ipam/service.md!}

View File

@ -95,7 +95,7 @@ The following methods are available to log results within a report:
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page. The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`. To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively. The status of a completed report is available as `self.failed` and the results object is `self.result`.
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last. By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.

View File

@ -2,7 +2,7 @@
## 1. Define the model class ## 1. Define the model class
Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be PrimaryModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module. Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be NetBoxModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module.
Each model should define, at a minimum: Each model should define, at a minimum:

View File

@ -11,17 +11,25 @@ Getting started with NetBox development is pretty straightforward, and should fe
### Fork the Repo ### Fork the Repo
Assuming you'll be working on your own fork, your first step will be to fork the [official git repository](https://github.com/netbox-community/netbox). (If you're a maintainer who's going to be working directly with the official repo, skip this step.) You can then clone your GitHub fork locally for development: Assuming you'll be working on your own fork, your first step will be to fork the [official git repository](https://github.com/netbox-community/netbox). (If you're a maintainer who's going to be working directly with the official repo, skip this step.) Click the "fork" button at top right (be sure that you've logged into GitHub first).
![GitHub fork button](../media/development/github_fork_button.png)
Copy the URL provided in the dialog box.
![GitHub fork dialog](../media/development/github_fork_dialog.png)
You can then clone your GitHub fork locally for development:
```no-highlight ```no-highlight
$ git clone https://github.com/youruseraccount/netbox.git $ git clone https://github.com/$username/netbox.git
Cloning into 'netbox'... Cloning into 'netbox'...
remote: Enumerating objects: 231, done. remote: Enumerating objects: 85949, done.
remote: Counting objects: 100% (231/231), done. remote: Counting objects: 100% (4672/4672), done.
remote: Compressing objects: 100% (147/147), done. remote: Compressing objects: 100% (1224/1224), done.
remote: Total 56705 (delta 134), reused 145 (delta 84), pack-reused 56474 remote: Total 85949 (delta 3538), reused 4332 (delta 3438), pack-reused 81277
Receiving objects: 100% (56705/56705), 27.96 MiB | 34.92 MiB/s, done. Receiving objects: 100% (85949/85949), 55.16 MiB | 44.90 MiB/s, done.
Resolving deltas: 100% (44177/44177), done. Resolving deltas: 100% (68008/68008), done.
$ ls netbox/ $ ls netbox/
base_requirements.txt contrib docs mkdocs.yml NOTICE requirements.txt upgrade.sh base_requirements.txt contrib docs mkdocs.yml NOTICE requirements.txt upgrade.sh
CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts
@ -33,7 +41,7 @@ The NetBox project utilizes three persistent git branches to track work:
* `develop` - All development on the upcoming stable release occurs here * `develop` - All development on the upcoming stable release occurs here
* `feature` - Tracks work on an upcoming major release * `feature` - Tracks work on an upcoming major release
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch, which receives merged only from the `develop` branch. Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0). For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0).
@ -60,7 +68,7 @@ $ python3 -m venv ~/.venv/netbox
This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`. This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`.
!!! info "Where to Create Your Virtual Environments" !!! info "Where to Create Your Virtual Environments"
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple venvs.
Once created, activate the virtual environment: Once created, activate the virtual environment:
@ -85,7 +93,7 @@ Collecting Django==3.1 (from -r requirements.txt (line 1))
### Configure NetBox ### Configure NetBox
Within the `netbox/netbox/` directory, copy `configuration.example.py` to `configuration.py` and update the following parameters: Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
* `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes * `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes
* `DATABASE`: PostgreSQL database connection parameters * `DATABASE`: PostgreSQL database connection parameters
@ -99,12 +107,13 @@ Within the `netbox/netbox/` directory, copy `configuration.example.py` to `confi
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command: Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command:
```no-highlight ```no-highlight
$ python netbox/manage.py runserver $ ./manage.py runserver
Watching for file changes with StatReloader
Performing system checks... Performing system checks...
System check identified no issues (0 silenced). System check identified no issues (0 silenced).
November 18, 2020 - 15:52:31 February 18, 2022 - 20:29:57
Django version 3.1, using settings 'netbox.settings' Django version 4.0.2, using settings 'netbox.settings'
Starting development server at http://127.0.0.1:8000/ Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C. Quit the server with CONTROL-C.
``` ```
@ -122,26 +131,36 @@ The demo data is provided in JSON format and loaded into an empty database using
## Running Tests ## Running Tests
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `/netbox/` directory, not the root directory of the repository. Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
When running tests, it's advised to use the special testing configuration file that ships with NetBox. This ensures that tests are run with the same configuration parameters consistently. To override your local configuration when running tests, set the `NETBOX_CONFIGURATION` environment variable to `netbox.configuration_testing`. To avoid potential issues with your local configuration file, set the `NETBOX_CONFIGURATION` to point to the packaged test configuration at `netbox/configuration_testing.py`. This will handle things like ensuring that the dummy plugin is enabled for comprehensive testing.
```no-highlight ```no-highlight
$ NETBOX_CONFIGURATION=netbox.configuration_testing python manage.py test $ export NETBOX_CONFIGURATION=netbox.configuration_testing
$ cd netbox/
$ python manage.py test
``` ```
In cases where you haven't made any changes to the database (which is most of the time), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.) In cases where you haven't made any changes to the database schema (which is typical), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
```no-highlight ```no-highlight
$ python manage.py test --keepdb $ python manage.py test --keepdb
``` ```
You can also limit the command to running only a specific subset of tests. For example, to run only IPAM and DCIM view tests: You can also reduce testing time by enabling parallel test execution with the `--parallel` flag. (By default, this will run as many parallel tests as you have processors. To avoid sluggishness, it's a good idea to specify a lower number of parallel tests.) This flag can be combined with `--keepdb`, although if you encounter any strange errors, try running the test suite again with parallelization disabled.
```no-highlight
$ python manage.py test --parallel <n>
```
Finally, it's possible to limit the run to a specific set of tests, specified by their Python path. For example, to run only IPAM and DCIM view tests:
```no-highlight ```no-highlight
$ python manage.py test dcim.tests.test_views ipam.tests.test_views $ python manage.py test dcim.tests.test_views ipam.tests.test_views
``` ```
This is handy for instances where just a few tests are failing and you want to re-run them individually.
## Submitting Pull Requests ## Submitting Pull Requests
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged. Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.

View File

@ -8,7 +8,7 @@ Check `base_requirements.txt` for any dependencies pinned to a specific version,
### Link to the Release Notes Page ### Link to the Release Notes Page
Add the release notes (`/docs/release-notes/X.Y.md`) to the table of contents within `mkdocs.yml`, and point `index.md` to the new file. Add the release notes (`/docs/release-notes/X.Y.md`) to the table of contents within `mkdocs.yml`, and add a summary of the major changes to `index.md`.
### Manually Perform a New Install ### Manually Perform a New Install

View File

@ -4,8 +4,11 @@ The `users.UserConfig` model holds individual preferences for each user in the f
## Available Preferences ## Available Preferences
| Name | Description | | Name | Description |
| ---- | ----------- | |--------------------------|---------------------------------------------------------------|
| extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) | | data_format | Preferred format when rendering raw data (JSON or YAML) |
| pagination.per_page | The number of items to display per page of a paginated table | | pagination.per_page | The number of items to display per page of a paginated table |
| tables.TABLE_NAME.columns | The ordered list of columns to display when viewing the table | | pagination.placement | Where to display the paginator controls relative to the table |
| tables.${table}.columns | The ordered list of columns to display when viewing the table |
| tables.${table}.ordering | A list of column names by which the table should be ordered |
| ui.colormode | Light or dark mode in the user interface |

View File

@ -54,7 +54,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
## Supported Python Versions ## Supported Python Versions
NetBox supports Python 3.7, 3.8, and 3.9 environments currently. (Support for Python 3.6 was removed in NetBox v3.0.) NetBox supports Python 3.8, 3.9, and 3.10 environments.
## Getting Started ## Getting Started

View File

@ -6,8 +6,8 @@ This section of the documentation discusses installing and configuring the NetBo
Begin by installing all system packages required by NetBox and its dependencies. Begin by installing all system packages required by NetBox and its dependencies.
!!! warning "Python 3.7 or later required" !!! warning "Python 3.8 or later required"
NetBox v3.0 and v3.1 require Python 3.7, 3.8, or 3.9. It is recommended to install at least Python v3.8, as this will become the minimum supported Python version in NetBox v3.2. NetBox v3.2 requires Python 3.8, 3.9, or 3.10.
=== "Ubuntu" === "Ubuntu"
@ -17,16 +17,11 @@ Begin by installing all system packages required by NetBox and its dependencies.
=== "CentOS" === "CentOS"
!!! warning
CentOS 8 does not provide Python 3.7 or later via its native package manager. You will need to install it via some other means. [Here is an example](https://tecadmin.net/install-python-3-7-on-centos-8/) of installing Python 3.7 from source.
Once you have Python 3.7 or later installed, install the remaining system packages:
```no-highlight ```no-highlight
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
``` ```
Before continuing, check that your installed Python version is at least 3.7: Before continuing, check that your installed Python version is at least 3.8:
```no-highlight ```no-highlight
python3 -V python3 -V
@ -117,11 +112,11 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
## Configuration ## Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. This file will hold all of your local configuration parameters. Move into the NetBox configuration directory and make a copy of `configuration_example.py` named `configuration.py`. This file will hold all of your local configuration parameters.
```no-highlight ```no-highlight
cd /opt/netbox/netbox/netbox/ cd /opt/netbox/netbox/netbox/
sudo cp configuration.example.py configuration.py sudo cp configuration_example.py configuration.py
``` ```
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations: Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
@ -234,10 +229,10 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
sudo /opt/netbox/upgrade.sh sudo /opt/netbox/upgrade.sh
``` ```
Note that **Python 3.7 or later is required** for NetBox v3.0 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.) Note that **Python 3.8 or later is required** for NetBox v3.2 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
```no-highlight ```no-highlight
sudo PYTHON=/usr/bin/python3.7 /opt/netbox/upgrade.sh sudo PYTHON=/usr/bin/python3.8 /opt/netbox/upgrade.sh
``` ```
!!! note !!! note

View File

@ -15,7 +15,7 @@ The following sections detail how to set up a new instance of NetBox:
| Dependency | Minimum Version | | Dependency | Minimum Version |
|------------|-----------------| |------------|-----------------|
| Python | 3.7 | | Python | 3.8 |
| PostgreSQL | 10 | | PostgreSQL | 10 |
| Redis | 4.0 | | Redis | 4.0 |

View File

@ -10,7 +10,7 @@ NetBox v3.0 and later require the following:
| Dependency | Minimum Version | | Dependency | Minimum Version |
|------------|-----------------| |------------|-----------------|
| Python | 3.7 | | Python | 3.8 |
| PostgreSQL | 10 | | PostgreSQL | 10 |
| Redis | 4.0 | | Redis | 4.0 |
@ -81,10 +81,10 @@ sudo ./upgrade.sh
``` ```
!!! warning !!! warning
If the default version of Python is not at least 3.7, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example: If the default version of Python is not at least 3.8, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
```no-highlight ```no-highlight
sudo PYTHON=/usr/bin/python3.7 ./upgrade.sh sudo PYTHON=/usr/bin/python3.8 ./upgrade.sh
``` ```
This script performs the following actions: This script performs the following actions:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -2,4 +2,4 @@
This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity. This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity.
Each provider network must be assigned to a provider. A circuit may terminate to either a provider network or to a site. Each provider network must be assigned to a provider, and may optionally be assigned an arbitrary service ID. A circuit may terminate to either a provider network or to a site.

View File

@ -5,4 +5,4 @@ Device bays represent a space or slot within a parent device in which a child de
Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices. Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices.
!!! note !!! note
Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device. Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, these should be modeled as modules installed within module bays.

View File

@ -1,3 +1,3 @@
## Device Bay Templates ## Device Bay Templates
A template for a device bay that will be created on all instantiations of the parent device type. A template for a device bay that will be created on all instantiations of the parent device type. Device bays hold child devices, such as blade servers.

View File

@ -4,13 +4,13 @@ A device type represents a particular make and model of hardware that exists in
Device types are instantiated as devices installed within sites and/or equipment racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple _instances_ of this type named "switch1," "switch2," and so on. Each device will automatically inherit the components (such as interfaces) of its device type at the time of creation. However, changes made to a device type will **not** apply to instances of that device type retroactively. Device types are instantiated as devices installed within sites and/or equipment racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple _instances_ of this type named "switch1," "switch2," and so on. Each device will automatically inherit the components (such as interfaces) of its device type at the time of creation. However, changes made to a device type will **not** apply to instances of that device type retroactively.
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following: Some devices house child devices which share physical resources, like space and power, but which function independently. A common example of this is blade server chassis. Each device type is designated as one of the following:
* A parent device (which has device bays) * A parent device (which has device bays)
* A child device (which must be installed within a device bay) * A child device (which must be installed within a device bay)
* Neither * Neither
!!! note !!! note
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device. This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device.
A device type may optionally specify an airflow direction, such as front-to-rear, rear-to-front, or passive. Airflow direction may also be set separately per device. If it is not defined for a device at the time of its creation, it will inherit the airflow setting of its device type. A device type may optionally specify an airflow direction, such as front-to-rear, rear-to-front, or passive. Airflow direction may also be set separately per device. If it is not defined for a device at the time of its creation, it will inherit the airflow setting of its device type.

View File

@ -1,6 +1,6 @@
## Interfaces ## Interfaces
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Additionally, each interface may optionally be assigned to a VRF.
!!! note !!! note
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa. Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.

View File

@ -1,7 +1,7 @@
# Inventory Items # Inventory Items
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Inventory items are distinct from other device components in that they cannot be templatized on a device type, and cannot be connected by cables. They are intended to be used primarily for inventory purposes. Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes.
Each inventory item can be assigned a manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside of NetBox). Each inventory item can be assigned a functional role, manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside NetBox).
Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface.

View File

@ -0,0 +1,3 @@
# Inventory Item Roles
Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc.

View File

@ -0,0 +1,3 @@
# Inventory Item Templates
A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any.

View File

@ -0,0 +1,5 @@
# Modules
A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch.
Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it. A module may optionally be assigned a serial number and asset tag.

View File

@ -0,0 +1,3 @@
## Module Bays
Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device.

View File

@ -0,0 +1,3 @@
## Module Bay Templates
A template for a module bay that will be created on all instantiations of the parent device type. Module bays hold installed modules that do not have an independent management plane, such as line cards.

View File

@ -0,0 +1,23 @@
# Module Types
A module type represent a specific make and model of hardware component which is installable within a device and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it.
Similar to device types, each module type can have any of the following component templates associated with it:
* Interfaces
* Console ports
* Console server ports
* Power ports
* Power Outlets
* Front pass-through ports
* Rear pass-through ports
Note that device bays and module bays may _not_ be added to modules.
## Automatic Component Renaming
When adding component templates to a module type, the string `{module}` can be used to reference the `position` field of the module bay into which an instance of the module type is being installed.
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
Automatic renaming is supported for all modular component types (those listed above).

View File

@ -19,6 +19,8 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
* JSON: Arbitrary data stored in JSON format * JSON: Arbitrary data stored in JSON format
* Selection: A selection of one of several pre-defined custom choices * Selection: A selection of one of several pre-defined custom choices
* Multiple selection: A selection field which supports the assignment of multiple values * Multiple selection: A selection field which supports the assignment of multiple values
* Object: A single NetBox object of the type defined by `object_type`
* Multiple object: One or more NetBox objects of the type defined by `object_type`
Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
@ -41,3 +43,7 @@ NetBox supports limited custom validation for custom field values. Following are
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
### Custom Object Fields
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.

View File

@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as:
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a> <a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
``` ```
Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links. Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually.
!!! warning !!! warning
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
@ -24,13 +24,14 @@ Custom links appear as buttons in the top right corner of the page. Numeric weig
The following context data is available within the template when rendering a custom link's text or URL. The following context data is available within the template when rendering a custom link's text or URL.
| Variable | Description | | Variable | Description |
|----------|-------------| |-----------|-------------------------------------------------------------------------------------------------------------------|
| `obj` | The NetBox object being displayed | | `object` | The NetBox object being displayed |
| `debug` | A boolean indicating whether debugging is enabled | | `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 |
| `request` | The current WSGI request | | `debug` | A boolean indicating whether debugging is enabled |
| `user` | The current user (if authenticated) | | `request` | The current WSGI request |
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user | | `user` | The current user (if authenticated) |
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user |
While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type. While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type.

View File

@ -3,7 +3,7 @@
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks. A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
!!! warning !!! warning
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. Webhooks support the inclusion of user-submitted code to generate URL, custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
## Configuration ## Configuration
@ -12,7 +12,7 @@ A webhook is a mechanism for conveying to some external system a change that too
* **Enabled** - If unchecked, the webhook will be inactive. * **Enabled** - If unchecked, the webhook will be inactive.
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected. * **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. * **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed. * **URL** - The fully-qualified URL of the request to be sent. This may specify a destination port number if needed. Jinja2 templating is supported for this field.
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`) * **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below). * **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.) * **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
@ -23,7 +23,7 @@ A webhook is a mechanism for conveying to some external system a change that too
## Jinja2 Template Support ## Jinja2 Template Support
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands. [Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration: For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:

View File

@ -0,0 +1,3 @@
# Service Templates
Service templates can be used to instantiate services on devices and virtual machines. A template defines a name, protocol, and port number(s), and may optionally include a description. Services can be instantiated from templates and applied to devices and/or virtual machines, and may be associated with specific IP addresses.

View File

@ -2,4 +2,6 @@
VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope. VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope.
A minimum and maximum child VLAN ID must be set for each group. (These default to 1 and 4094 respectively.) VLANs created within a group must have a VID that falls between these values (inclusive).
Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.

View File

@ -1,3 +1,3 @@
## Interfaces ## Interfaces
Virtual machine interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them. Virtual machine interfaces behave similarly to device interfaces, and can be assigned to VRFs, and may have IP addresses, VLANs, and services attached to them. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them.

View File

@ -1,432 +0,0 @@
# Plugin Development
!!! info "Help Improve the NetBox Plugins Framework!"
We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338).
This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox.
Plugins can do a lot, including:
* Create Django models to store data in the database
* Provide their own "pages" (views) in the web user interface
* Inject template content and navigation links
* Establish their own REST API endpoints
* Add custom request/response middleware
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
!!! warning
While very powerful, the NetBox plugins API is necessarily limited in its scope. The plugins API is discussed here in its entirety: Any part of the NetBox code base not documented here is _not_ part of the supported plugins API, and should not be employed by a plugin. Internal elements of NetBox are subject to change at any time and without warning. Plugin authors are **strongly** encouraged to develop plugins using only the officially supported components discussed here and those provided by the underlying Django framework so as to avoid breaking changes in future releases.
## Initial Setup
### Plugin Structure
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this:
```no-highlight
project-name/
- plugin_name/
- templates/
- plugin_name/
- *.html
- __init__.py
- middleware.py
- navigation.py
- signals.py
- template_content.py
- urls.py
- views.py
- README
- setup.py
```
The top level is the project root, which can have any name that you like. Immediately within the root should exist several items:
* `setup.py` - This is a standard installation script used to install the plugin package within the Python environment.
* `README` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write README files using a markup language such as Markdown.
* The plugin source directory, with the same name as your plugin. This must be a valid Python package name (e.g. no spaces or hyphens).
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class.
### Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.7/distutils/setupscript.html) we'll use to install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to inform the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
```python
from setuptools import find_packages, setup
setup(
name='netbox-animal-sounds',
version='0.1',
description='An example NetBox plugin',
url='https://github.com/netbox-community/netbox-animal-sounds',
author='Jeremy Stretch',
license='Apache 2.0',
install_requires=[],
packages=find_packages(),
include_package_data=True,
zip_safe=False,
)
```
Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html).
!!! note
`zip_safe=False` is **required** as the current plugin iteration is not zip safe due to upstream python issue [issue19699](https://bugs.python.org/issue19699)
### Define a PluginConfig
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
```python
from extras.plugins import PluginConfig
class AnimalSoundsConfig(PluginConfig):
name = 'netbox_animal_sounds'
verbose_name = 'Animal Sounds'
description = 'An example plugin for development purposes'
version = '0.1'
author = 'Jeremy Stretch'
author_email = 'author@example.com'
base_url = 'animal-sounds'
required_settings = []
default_settings = {
'loud': False
}
config = AnimalSoundsConfig
```
NetBox looks for the `config` variable within a plugin's `__init__.py` to load its configuration. Typically, this will be set to the PluginConfig subclass, but you may wish to dynamically generate a PluginConfig class based on environment variables or other factors.
#### PluginConfig Attributes
| Name | Description |
| ---- | ----------- |
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
### Create a Virtual Environment
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) specific to your plugin. This will afford you complete control over the installed versions of all dependencies and avoid conflicting with any system packages. This environment can live wherever you'd like, however it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
```shell
python3 -m venv /path/to/my/venv
```
You can make NetBox available within this environment by creating a path file pointing to its location. This will add NetBox to the Python path upon activation. (Be sure to adjust the command below to specify your actual virtual environment path, Python version, and NetBox installation.)
```shell
cd $VENV/lib/python3.7/site-packages/
echo /opt/netbox/netbox > netbox.pth
```
### Install the Plugin for Development
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
```no-highlight
$ python setup.py develop
```
## Database Models
If your plugin introduces a new type of object in NetBox, you'll probably want to create a [Django model](https://docs.djangoproject.com/en/stable/topics/db/models/) for it. A model is essentially a Python representation of a database table, with attributes that represent individual columns. Model instances can be created, manipulated, and deleted using [queries](https://docs.djangoproject.com/en/stable/topics/db/queries/). Models must be defined within a file named `models.py`.
Below is an example `models.py` file containing a model with two character fields:
```python
from django.db import models
class Animal(models.Model):
name = models.CharField(max_length=50)
sound = models.CharField(max_length=50)
def __str__(self):
return self.name
```
Once you have defined the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command.
!!! note
A plugin must be installed before it can be used with Django management commands. If you skipped this step above, run `python setup.py develop` from the plugin's root directory.
```no-highlight
$ ./manage.py makemigrations netbox_animal_sounds
Migrations for 'netbox_animal_sounds':
/home/jstretch/animal_sounds/netbox_animal_sounds/migrations/0001_initial.py
- Create model Animal
```
Next, we can apply the migration to the database with the `migrate` command:
```no-highlight
$ ./manage.py migrate netbox_animal_sounds
Operations to perform:
Apply all migrations: netbox_animal_sounds
Running migrations:
Applying netbox_animal_sounds.0001_initial... OK
```
For more background on schema migrations, see the [Django documentation](https://docs.djangoproject.com/en/stable/topics/migrations/).
### Using the Django Admin Interface
Plugins can optionally expose their models via Django's built-in [administrative interface](https://docs.djangoproject.com/en/stable/ref/contrib/admin/). This can greatly improve troubleshooting ability, particularly during development. To expose a model, simply register it using Django's `admin.register()` function. An example `admin.py` file for the above model is shown below:
```python
from django.contrib import admin
from .models import Animal
@admin.register(Animal)
class AnimalAdmin(admin.ModelAdmin):
list_display = ('name', 'sound')
```
This will display the plugin and its model in the admin UI. Staff users can create, change, and delete model instances via the admin UI without needing to create a custom view.
![NetBox plugin in the admin UI](../media/plugins/plugin_admin_ui.png)
## Views
If your plugin needs its own page or pages in the NetBox web UI, you'll need to define views. A view is a particular page tied to a URL within NetBox, which renders content using a template. Views are typically defined in `views.py`, and URL patterns in `urls.py`. As an example, let's write a view which displays a random animal and the sound it makes. First, we'll create the view in `views.py`:
```python
from django.shortcuts import render
from django.views.generic import View
from .models import Animal
class RandomAnimalView(View):
"""
Display a randomly-selected animal.
"""
def get(self, request):
animal = Animal.objects.order_by('?').first()
return render(request, 'netbox_animal_sounds/animal.html', {
'animal': animal,
})
```
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create a template named `animal.html` as described below.
### Extending the Base Template
NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This template includes four content blocks:
* `title` - The page title
* `header` - The upper portion of the page
* `content` - The main page body
* `javascript` - A section at the end of the page for including Javascript code
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
```jinja2
{% extends 'base/layout.html' %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
<h2 class="text-center" style="margin-top: 200px">
{% if animal %}
The {{ animal.name|lower }} says
{% if config.loud %}
{{ animal.sound|upper }}!
{% else %}
{{ animal.sound }}
{% endif %}
{% else %}
No animals have been created yet!
{% endif %}
</h2>
{% endwith %}
{% endblock %}
```
The first line of the template instructs Django to extend the NetBox base template and inject our custom content within its `content` block.
!!! note
Django renders templates with its own custom [template language](https://docs.djangoproject.com/en/stable/topics/templates/#the-django-template-language). This is very similar to Jinja2, however there are some important differences to be aware of.
Finally, to make the view accessible to users, we need to register a URL for it. We do this in `urls.py` by defining a `urlpatterns` variable containing a list of paths.
```python
from django.urls import path
from . import views
urlpatterns = [
path('random/', views.RandomAnimalView.as_view(), name='random_animal'),
]
```
A URL pattern has three components:
* `route` - The unique portion of the URL dedicated to this view
* `view` - The view itself
* `name` - A short name used to identify the URL path internally
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
## REST API Endpoints
Plugins can declare custom endpoints on NetBox's REST API to retrieve or manipulate models or other data. These behave very similarly to views, except that instead of rendering arbitrary content using a template, data is returned in JSON format using a serializer. NetBox uses the [Django REST Framework](https://www.django-rest-framework.org/), which makes writing API serializers and views very simple.
First, we'll create a serializer for our `Animal` model, in `api/serializers.py`:
```python
from rest_framework.serializers import ModelSerializer
from netbox_animal_sounds.models import Animal
class AnimalSerializer(ModelSerializer):
class Meta:
model = Animal
fields = ('id', 'name', 'sound')
```
Next, we'll create a generic API view set that allows basic CRUD (create, read, update, and delete) operations for Animal instances. This is defined in `api/views.py`:
```python
from rest_framework.viewsets import ModelViewSet
from netbox_animal_sounds.models import Animal
from .serializers import AnimalSerializer
class AnimalViewSet(ModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
```
Finally, we'll register a URL for our endpoint in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
```python
from rest_framework import routers
from .views import AnimalViewSet
router = routers.DefaultRouter()
router.register('animals', AnimalViewSet)
urlpatterns = router.urls
```
With these three components in place, we can request `/api/plugins/animal-sounds/animals/` to retrieve a list of all Animal objects defined.
![NetBox REST API plugin endpoint](../media/plugins/plugin_rest_api_endpoint.png)
!!! warning
This example is provided as a minimal reference implementation only. It does not address authentication, performance, or myriad other concerns that plugin authors should have.
## Navigation Menu Items
To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu under the "Plugins" header. Menu items are added by defining a list of PluginMenuItem instances. By default, this should be a variable named `menu_items` in the file `navigation.py`. An example is shown below.
```python
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
menu_items = (
PluginMenuItem(
link='plugins:netbox_animal_sounds:random_animal',
link_text='Random sound',
buttons=(
PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE),
PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN),
)
),
)
```
A `PluginMenuItem` has the following attributes:
* `link` - The name of the URL path to which this menu item links
* `link_text` - The text presented to the user
* `permissions` - A list of permissions required to display this link (optional)
* `buttons` - An iterable of PluginMenuButton instances to display (optional)
A `PluginMenuButton` has the following attributes:
* `link` - The name of the URL path to which this button links
* `title` - The tooltip text (displayed when the mouse hovers over the button)
* `icon_class` - Button icon CSS class (NetBox currently supports [Font Awesome 4.7](https://fontawesome.com/v4.7.0/icons/))
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
* `permissions` - A list of permissions required to display this button (optional)
!!! note
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
## Extending Core Templates
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
* `left_page()` - Inject content on the left side of the page
* `right_page()` - Inject content on the right side of the page
* `full_width_page()` - Inject content across the entire bottom of the page
* `buttons()` - Add buttons to the top of the page
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
* `object` - The object being viewed
* `request` - The current request
* `settings` - Global NetBox settings
* `config` - Plugin-specific configuration parameters
For example, accessing `{{ request.user }}` within a template will return the current user.
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
```python
from extras.plugins import PluginTemplateExtension
from .models import Animal
class SiteAnimalCount(PluginTemplateExtension):
model = 'dcim.site'
def right_page(self):
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
'animal_count': Animal.objects.count(),
})
template_extensions = [SiteAnimalCount]
```
## Background Tasks
By default, Netbox provides 3 differents [RQ](https://python-rq.org/) queues to run background jobs : *high*, *default* and *low*.
These 3 core queues can be used out-of-the-box by plugins to define background tasks.
Plugins can also define dedicated queues. These queues can be configured under the PluginConfig class `queues` attribute. An example configuration
is below:
```python
class MyPluginConfig(PluginConfig):
name = 'myplugin'
...
queues = [
'queue1',
'queue2',
'queue-whatever-the-name'
]
```
The PluginConfig above creates 3 queues with the following names: *myplugin.queue1*, *myplugin.queue2*, *myplugin.queue-whatever-the-name*.
As you can see, the queue's name is always preprended with the plugin's name, to avoid any name clashes between different plugins.
In case you create dedicated queues for your plugin, it is strongly advised to also create a dedicated RQ worker instance. This instance should only listen to the queues defined in your plugin - to avoid impact between your background tasks and netbox internal tasks.
```
python manage.py rqworker myplugin.queue1 myplugin.queue2 myplugin.queue-whatever-the-name
```

View File

@ -0,0 +1,30 @@
# Background Tasks
NetBox supports the queuing of tasks that need to be performed in the background, decoupled from the request-response cycle, using the [Python RQ](https://python-rq.org/) library. Three task queues of differing priority are defined by default:
* High
* Default
* Low
Any tasks in the "high" queue are completed before the default queue is checked, and any tasks in the default queue are completed before those in the "low" queue.
Plugins can also add custom queues for their own needs by setting the `queues` attribute under the PluginConfig class. An example is included below:
```python
class MyPluginConfig(PluginConfig):
name = 'myplugin'
...
queues = [
'foo',
'bar',
]
```
The PluginConfig above creates two custom queues with the following names `my_plugin.foo` and `my_plugin.bar`. (The plugin's name is prepended to each queue to avoid conflicts between plugins.)
!!! warning "Configuring the RQ worker process"
By default, NetBox's RQ worker process only services the high, default, and low queues. Plugins which introduce custom queues should advise users to either reconfigure the default worker, or run a dedicated worker specifying the necessary queues. For example:
```
python manage.py rqworker my_plugin.foo my_plugin.bar
```

View File

@ -0,0 +1,70 @@
# Filters & Filter Sets
Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI, REST API, or GraphQL API. NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets.
## FilterSet Classes
To support additional functionality standard to NetBox models, such as tag assignment and custom field support, the `NetBoxModelFilterSet` class is available for use by plugins. This should be used as the base filter set class for plugin models which inherit from `NetBoxModel`. Within this class, individual filters can be declared as directed by the `django-filters` documentation. An example is provided below.
```python
# filtersets.py
import django_filters
from netbox.filtersets import NetBoxModelFilterSet
from .models import MyModel
class MyFilterSet(NetBoxModelFilterSet):
status = django_filters.MultipleChoiceFilter(
choices=(
('foo', 'Foo'),
('bar', 'Bar'),
('baz', 'Baz'),
),
null_value=None
)
class Meta:
model = MyModel
fields = ('some', 'other', 'fields')
```
### Declaring Filter Sets
To utilize a filter set in a subclass of one of NetBox's generic views (such as `ObjectListView` or `BulkEditView`), define the `filterset` attribute on the view class:
```python
# views.py
from netbox.views.generic import ObjectListView
from .filtersets import MyModelFitlerSet
from .models import MyModel
class MyModelListView(ObjectListView):
queryset = MyModel.objects.all()
filterset = MyModelFitlerSet
```
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
```python
# api/views.py
from myplugin import models, filtersets
from . import serializers
class MyModelViewSet(...):
queryset = models.MyModel.objects.all()
serializer_class = serializers.MyModelSerializer
filterset_class = filtersets.MyModelFilterSet
```
## Filter Classes
### TagFilter
The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class.
```python
from django_filters import FilterSet
from extras.filters import TagFilter
class MyModelFilterSet(FilterSet):
tag = TagFilter()
```

View File

@ -0,0 +1,216 @@
# Forms
## Form Classes
NetBox provides several base form classes for use by plugins.
| Form Class | Purpose |
|---------------------------|--------------------------------------|
| `NetBoxModelForm` | Create/edit individual objects |
| `NetBoxModelCSVForm` | Bulk import objects from CSV data |
| `NetBoxModelBulkEditForm` | Edit multiple objects simultaneously |
| `NetBoxModelFilterSetForm` | Filter objects within a list view |
### `NetBoxModelForm`
This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields.
| Attribute | Description |
|-------------|-------------------------------------------------------------|
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
**Example**
```python
from dcim.models import Site
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField
from .models import MyModel
class MyModelForm(NetBoxModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all()
)
comments = CommentField()
fieldsets = (
('Model Stuff', ('name', 'status', 'site', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
class Meta:
model = MyModel
fields = ('name', 'status', 'site', 'comments', 'tags')
```
!!! tip "Comment fields"
If your form has a `comments` field, there's no need to list it; this will always appear last on the page.
### `NetBoxModelCSVForm`
This form facilitates the bulk import of new objects from CSV data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
**Example**
```python
from dcim.models import Site
from netbox.forms import NetBoxModelCSVForm
from utilities.forms import CSVModelChoiceField
from .models import MyModel
class MyModelCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Assigned site'
)
class Meta:
model = MyModel
fields = ('name', 'status', 'site', 'comments')
```
### `NetBoxModelBulkEditForm`
This form facilitates editing multiple objects in bulk. Unlike a model form, this form does not have a child `Meta` class, and must explicitly define each field. All fields in a bulk edit form are generally declared with `required=False`.
| Attribute | Description |
|-------------------|---------------------------------------------------------------------------------------------|
| `model` | The model of object being edited |
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
**Example**
```python
from django import forms
from dcim.models import Site
from netbox.forms import NetBoxModelCSVForm
from utilities.forms import CommentField, DynamicModelChoiceField
from .models import MyModel, MyModelStatusChoices
class MyModelEditForm(NetBoxModelCSVForm):
name = forms.CharField(
required=False
)
status = forms.ChoiceField(
choices=MyModelStatusChoices,
required=False
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
)
comments = CommentField()
model = MyModel
fieldsets = (
('Model Stuff', ('name', 'status', 'site')),
)
nullable_fields = ('site', 'comments')
```
### `NetBoxModelFilterSetForm`
This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set.
| Attribute | Description |
|-------------------|-------------------------------------------------------------|
| `model` | The model of object being edited |
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
**Example**
```python
from dcim.models import Site
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField
from .models import MyModel, MyModelStatusChoices
class MyModelFilterForm(NetBoxModelFilterSetForm):
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False
)
status = MultipleChoiceField(
choices=MyModelStatusChoices,
required=False
)
model = MyModel
```
## General Purpose Fields
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
::: utilities.forms.ColorField
selection:
members: false
::: utilities.forms.CommentField
selection:
members: false
::: utilities.forms.JSONField
selection:
members: false
::: utilities.forms.MACAddressField
selection:
members: false
::: utilities.forms.SlugField
selection:
members: false
## Choice Fields
::: utilities.forms.ChoiceField
selection:
members: false
::: utilities.forms.MultipleChoiceField
selection:
members: false
## Dynamic Object Fields
::: utilities.forms.DynamicModelChoiceField
selection:
members: false
::: utilities.forms.DynamicModelMultipleChoiceField
selection:
members: false
## Content Type Fields
::: utilities.forms.ContentTypeChoiceField
selection:
members: false
::: utilities.forms.ContentTypeMultipleChoiceField
selection:
members: false
## CSV Import Fields
::: utilities.forms.CSVChoiceField
selection:
members: false
::: utilities.forms.CSVMultipleChoiceField
selection:
members: false
::: utilities.forms.CSVModelChoiceField
selection:
members: false
::: utilities.forms.CSVContentTypeField
selection:
members: false
::: utilities.forms.CSVMultipleContentTypeField
selection:
members: false

View File

@ -0,0 +1,52 @@
# GraphQL API
## Defining the Schema Class
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class. This class must be a subclass of `graphene.ObjectType`.
### Example
```python
# graphql.py
import graphene
from netbox.graphql.types import NetBoxObjectType
from netbox.graphql.fields import ObjectField, ObjectListField
from . import filtersets, models
class MyModelType(NetBoxObjectType):
class Meta:
model = models.MyModel
fields = '__all__'
filterset_class = filtersets.MyModelFilterSet
class MyQuery(graphene.ObjectType):
mymodel = ObjectField(MyModelType)
mymodel_list = ObjectListField(MyModelType)
schema = MyQuery
```
## GraphQL Objects
NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType
selection:
members: false
::: netbox.graphql.types.NetBoxObjectType
selection:
members: false
## GraphQL Fields
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
selection:
members: false
::: netbox.graphql.fields.ObjectListField
selection:
members: false

View File

@ -0,0 +1,171 @@
# Plugins Development
!!! tip "Plugins Development Tutorial"
Just getting started with plugins? Check out our [**NetBox Plugin Tutorial**](https://github.com/netbox-community/netbox-plugin-tutorial) on GitHub! This in-depth guide will walk you through the process of creating an entire plugin from scratch. It even includes a companion [demo plugin repo](https://github.com/netbox-community/netbox-plugin-demo) to ensure you can jump in at any step along the way. This will get you up and running with plugins in no time!
NetBox can be extended to support additional data models and functionality through the use of plugins. A plugin is essentially a self-contained [Django app](https://docs.djangoproject.com/en/stable/) which gets installed alongside NetBox to provide custom functionality. Multiple plugins can be installed in a single NetBox instance, and each plugin can be enabled and configured independently.
!!! info "Django Development"
Django is the Python framework on which NetBox is built. As Django itself is very well-documented, this documentation covers only the aspects of plugin development which are unique to NetBox.
Plugins can do a lot, including:
* Create Django models to store data in the database
* Provide their own "pages" (views) in the web user interface
* Inject template content and navigation links
* Extend NetBox's REST and GraphQL APIs
* Add custom request/response middleware
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
!!! warning
While very powerful, the NetBox plugins API is necessarily limited in its scope. The plugins API is discussed here in its entirety: Any part of the NetBox code base not documented here is _not_ part of the supported plugins API, and should not be employed by a plugin. Internal elements of NetBox are subject to change at any time and without warning. Plugin authors are **strongly** encouraged to develop plugins using only the officially supported components discussed here and those provided by the underlying Django framework to avoid breaking changes in future releases.
## Plugin Structure
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin might look something like this:
```no-highlight
project-name/
- plugin_name/
- api/
- __init__.py
- serializers.py
- urls.py
- views.py
- migrations/
- __init__.py
- 0001_initial.py
- ...
- templates/
- plugin_name/
- *.html
- __init__.py
- filtersets.py
- graphql.py
- models.py
- middleware.py
- navigation.py
- signals.py
- tables.py
- template_content.py
- urls.py
- views.py
- README.md
- setup.py
```
The top level is the project root, which can have any name that you like. Immediately within the root should exist several items:
* `setup.py` - This is a standard installation script used to install the plugin package within the Python environment.
* `README.md` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write `README` files using a markup language such as Markdown to enable human-friendly display.
* The plugin source directory. This must be a valid Python package name, typically comprising only lowercase letters, numbers, and underscores.
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class, discussed below.
## PluginConfig
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
```python
from extras.plugins import PluginConfig
class FooBarConfig(PluginConfig):
name = 'foo_bar'
verbose_name = 'Foo Bar'
description = 'An example NetBox plugin'
version = '0.1'
author = 'Jeremy Stretch'
author_email = 'author@example.com'
base_url = 'foo-bar'
required_settings = []
default_settings = {
'baz': True
}
config = FooBarConfig
```
NetBox looks for the `config` variable within a plugin's `__init__.py` to load its configuration. Typically, this will be set to the PluginConfig subclass, but you may wish to dynamically generate a PluginConfig class based on environment variables or other factors.
### PluginConfig Attributes
| Name | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
## Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
```python
from setuptools import find_packages, setup
setup(
name='my-example-plugin',
version='0.1',
description='An example NetBox plugin',
url='https://github.com/jeremystretch/my-example-plugin',
author='Jeremy Stretch',
license='Apache 2.0',
install_requires=[],
packages=find_packages(),
include_package_data=True,
zip_safe=False,
)
```
Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html).
!!! info
`zip_safe=False` is **required** as the current plugin iteration is not zip safe due to upstream python issue [issue19699](https://bugs.python.org/issue19699)
## Create a Virtual Environment
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like, however it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
```shell
python3 -m venv ~/.virtualenvs/my_plugin
```
You can make NetBox available within this environment by creating a path file pointing to its location. This will add NetBox to the Python path upon activation. (Be sure to adjust the command below to specify your actual virtual environment path, Python version, and NetBox installation.)
```shell
echo /opt/netbox/netbox > $VENV/lib/python3.8/site-packages/netbox.pth
```
## Development Installation
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
```no-highlight
$ python setup.py develop
```
## Configure NetBox
To enable the plugin in NetBox, add it to the `PLUGINS` parameter in `configuration.py`:
```python
PLUGINS = [
'my_plugin',
]
```

View File

@ -0,0 +1,185 @@
# Database Models
## Creating Models
If your plugin introduces a new type of object in NetBox, you'll probably want to create a [Django model](https://docs.djangoproject.com/en/stable/topics/db/models/) for it. A model is essentially a Python representation of a database table, with attributes that represent individual columns. Instances of a model (objects) can be created, manipulated, and deleted using [queries](https://docs.djangoproject.com/en/stable/topics/db/queries/). Models must be defined within a file named `models.py`.
Below is an example `models.py` file containing a model with two character (text) fields:
```python
from django.db import models
class MyModel(models.Model):
foo = models.CharField(max_length=50)
bar = models.CharField(max_length=50)
def __str__(self):
return f'{self.foo} {self.bar}'
```
Every model includes by default a numeric primary key. This value is generated automatically by the database, and can be referenced as `pk` or `id`.
## Enabling NetBox Features
Plugin models can leverage certain NetBox features by inheriting from NetBox's `NetBoxModel` class. This class extends the plugin model to enable features unique to NetBox, including:
* Change logging
* Custom fields
* Custom links
* Custom validation
* Export templates
* Journaling
* Tags
* Webhooks
This class performs two crucial functions:
1. Apply any fields, methods, and/or attributes necessary to the operation of these features
2. Register the model with NetBox as utilizing these features
Simply subclass BaseModel when defining a model in your plugin:
```python
# models.py
from django.db import models
from netbox.models import NetBoxModel
class MyModel(NetBoxModel):
foo = models.CharField()
...
```
### Enabling Features Individually
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)
For example, if we wanted to support only tags and export templates, we would inherit from NetBox's `ExportTemplatesMixin` and `TagsMixin` classes, and from Django's `Model` class. (Inheriting _all_ the available mixins is essentially the same as subclassing `NetBoxModel`.)
```python
# models.py
from django.db import models
from netbox.models.features import ExportTemplatesMixin, TagsMixin
class MyModel(ExportTemplatesMixin, TagsMixin, models.Model):
foo = models.CharField()
...
```
## Database Migrations
Once you have completed defining the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command. (Ensure that your plugin has been installed and enabled first, otherwise it won't be found.)
!!! note Enable Developer Mode
NetBox enforces a safeguard around the `makemigrations` command to protect regular users from inadvertently creating erroneous schema migrations. To enable this command for plugin development, set `DEVELOPER=True` in `configuration.py`.
```no-highlight
$ ./manage.py makemigrations my_plugin
Migrations for 'my_plugin':
/home/jstretch/animal_sounds/my_plugin/migrations/0001_initial.py
- Create model MyModel
```
Next, we can apply the migration to the database with the `migrate` command:
```no-highlight
$ ./manage.py migrate my_plugin
Operations to perform:
Apply all migrations: my_plugin
Running migrations:
Applying my_plugin.0001_initial... OK
```
For more information about database migrations, see the [Django documentation](https://docs.djangoproject.com/en/stable/topics/migrations/).
## Feature Mixins Reference
!!! warning
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `features` module, they are not yet supported for use by plugins.
::: netbox.models.features.ChangeLoggingMixin
::: netbox.models.features.CustomLinksMixin
::: netbox.models.features.CustomFieldsMixin
::: netbox.models.features.CustomValidationMixin
::: netbox.models.features.ExportTemplatesMixin
::: netbox.models.features.JournalingMixin
::: netbox.models.features.TagsMixin
::: netbox.models.features.WebhooksMixin
## Choice Sets
For model fields which support the selection of one or more values from a predefined list of choices, NetBox provides the `ChoiceSet` utility class. This can be used in place of a regular choices tuple to provide enhanced functionality, namely dynamic configuration and colorization. (See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/models/fields/#choices) on the `choices` parameter for supported model fields.)
To define choices for a model field, subclass `ChoiceSet` and define a tuple named `CHOICES`, of which each member is a two- or three-element tuple. These elements are:
* The database value
* The corresponding human-friendly label
* The assigned color (optional)
A complete example is provided below.
!!! note
Authors may find it useful to declare each of the database values as constants on the class, and reference them within `CHOICES` members. This convention allows the values to be referenced from outside the class, however it is not strictly required.
### Dynamic Configuration
Some model field choices in NetBox can be configured by an administrator. For example, the default values for the Site model's `status` field can be replaced or supplemented with custom choices. To enable dynamic configuration for a ChoiceSet subclass, define its `key` as a string specifying the model and field name to which it applies. For example:
```python
from utilities.choices import ChoiceSet
class StatusChoices(ChoiceSet):
key = 'MyModel.status'
```
To extend or replace the default values for this choice set, a NetBox administrator can then reference it under the [`FIELD_CHOICES`](../../configuration/optional-settings.md#field_choices) configuration parameter. For example, the `status` field on `MyModel` in `my_plugin` would be referenced as:
```python
FIELD_CHOICES = {
'my_plugin.MyModel.status': (
# Custom choices
)
}
```
### Example
```python
# choices.py
from utilities.choices import ChoiceSet
class StatusChoices(ChoiceSet):
key = 'MyModel.status'
STATUS_FOO = 'foo'
STATUS_BAR = 'bar'
STATUS_BAZ = 'baz'
CHOICES = [
(STATUS_FOO, 'Foo', 'red'),
(STATUS_BAR, 'Bar', 'green'),
(STATUS_BAZ, 'Baz', 'blue'),
]
```
!!! warning
For dynamic configuration to work properly, `CHOICES` must be a mutable list, rather than a tuple.
```python
# models.py
from django.db import models
from .choices import StatusChoices
class MyModel(models.Model):
status = models.CharField(
max_length=50,
choices=StatusChoices,
default=StatusChoices.STATUS_FOO
)
```

View File

@ -0,0 +1,50 @@
# Navigation
## Menu Items
To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu under the "Plugins" header. Menu items are added by defining a list of PluginMenuItem instances. By default, this should be a variable named `menu_items` in the file `navigation.py`. An example is shown below.
!!! tip
The path to declared menu items can be modified by setting `menu_items` in the PluginConfig instance.
```python
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
menu_items = (
PluginMenuItem(
link='plugins:netbox_animal_sounds:random_animal',
link_text='Random sound',
buttons=(
PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE),
PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN),
)
),
)
```
A `PluginMenuItem` has the following attributes:
| Attribute | Required | Description |
|---------------|----------|------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this menu item links |
| `link_text` | Yes | The text presented to the user |
| `permissions` | - | A list of permissions required to display this link |
| `buttons` | - | An iterable of PluginMenuButton instances to include |
## Menu Buttons
A `PluginMenuButton` has the following attributes:
| Attribute | Required | Description |
|---------------|----------|--------------------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this button links |
| `title` | Yes | The tooltip text (displayed when the mouse hovers over the button) |
| `icon_class` | Yes | Button icon CSS class* |
| `color` | - | One of the choices provided by `ButtonColorChoices` |
| `permissions` | - | A list of permissions required to display this button |
*NetBox supports [Material Design Icons](https://materialdesignicons.com/).
!!! note
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.

View File

@ -0,0 +1,117 @@
# REST API
Plugins can declare custom endpoints on NetBox's REST API to retrieve or manipulate models or other data. These behave very similarly to views, except that instead of rendering arbitrary content using a template, data is returned in JSON format using a serializer.
Generally speaking, there aren't many NetBox-specific components to implementing REST API functionality in a plugin. NetBox employs the [Django REST Framework](https://www.django-rest-framework.org/) (DRF) for its REST API, and plugin authors will find that they can largely replicate the same patterns found in NetBox's implementation. Some brief examples are included here for reference.
## Code Layout
The recommended approach is to separate API serializers, views, and URLs into separate modules under the `api/` directory to keep things tidy, particularly for larger projects. The file at `api/__init__.py` can import the relevant components from each submodule to allow import all API components directly from elsewhere. However, this is merely a convention and not strictly required.
```no-highlight
project-name/
- plugin_name/
- api/
- __init__.py
- serializers.py
- urls.py
- views.py
...
```
## Serializers
### Model Serializers
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
#### Example
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class. It is generally advisable to include a `url` attribute on each serializer. This will render the direct link to access the object being rendered.
```python
# api/serializers.py
from rest_framework import serializers
from netbox.api.serializers import NetBoxModelSerializer
from my_plugin.models import MyModel
class MyModelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:myplugin-api:mymodel-detail'
)
class Meta:
model = MyModel
fields = ('id', 'foo', 'bar', 'baz')
```
### Nested Serializers
There are two cases where it is generally desirable to show only a minimal representation of an object:
1. When displaying an object related to the one being viewed (for example, the region to which a site is assigned)
2. Listing many objects using "brief" mode
To accommodate these, it is recommended to create nested serializers accompanying the "full" serializer for each model. NetBox provides the `WritableNestedSerializer` class for just this purpose. This class accepts a primary key value on write, but displays an object representation for read requests. It also includes a read-only `display` attribute which conveys the string representation of the object.
#### Example
```python
# api/serializers.py
from rest_framework import serializers
from netbox.api.serializers import WritableNestedSerializer
from my_plugin.models import MyModel
class NestedMyModelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:myplugin-api:mymodel-detail'
)
class Meta:
model = MyModel
fields = ('id', 'display', 'foo')
```
## Viewsets
Just as in the user interface, a REST API view handles the business logic of displaying and interacting with NetBox objects. NetBox provides the `NetBoxModelViewSet` class, which extends DRF's built-in `ModelViewSet` to handle bulk operations and object validation.
Unlike the user interface, typically only a single view set is required per model: This view set handles all request types (`GET`, `POST`, `DELETE`, etc.).
### Example
To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/views.py`, and define the `queryset` and `serializer_class` attributes.
```python
# api/views.py
from netbox.api.viewsets import ModelViewSet
from my_plugin.models import MyModel
from .serializers import MyModelSerializer
class MyModelViewSet(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
```
## Routers
Routers map URLs to REST API views (endpoints). NetBox does not provide any custom components for this; the [`DefaultRouter`](https://www.django-rest-framework.org/api-guide/routers/#defaultrouter) class provided by DRF should suffice for most use cases.
Routers should be exposed in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
### Example
```python
# api/urls.py
from netbox.api.routers import NetBoxRouter
from .views import MyModelViewSet
router = NetBoxRouter()
router.register('my-model', MyModelViewSet)
urlpatterns = router.urls
```
This will make the plugin's view accessible at `/api/plugins/my-plugin/my-model/`.
!!! warning
The examples provided here are intended to serve as a minimal reference implementation only. This documentation does not address authentication, performance, or myriad other concerns that plugin authors may need to address.

View File

@ -0,0 +1,88 @@
# Tables
NetBox employs the [`django-tables2`](https://django-tables2.readthedocs.io/) library for rendering dynamic object tables. These tables display lists of objects, and can be sorted and filtered by various parameters.
## NetBoxTable
To provide additional functionality beyond what is supported by the stock `Table` class in `django-tables2`, NetBox provides the `NetBoxTable` class. This custom table class includes support for:
* User-configurable column display and ordering
* Custom field & custom link columns
* Automatic prefetching of related objects
It also includes several default columns:
* `pk` - A checkbox for selecting the object associated with each table row (where applicable)
* `id` - The object's numeric database ID, as a hyperlink to the object's view (hidden by default)
* `actions` - A dropdown menu presenting object-specific actions available to the user
### Example
```python
# tables.py
import django_tables2 as tables
from netbox.tables import NetBoxTable
from .models import MyModel
class MyModelTable(NetBoxTable):
name = tables.Column(
linkify=True
)
...
class Meta(NetBoxTable.Meta):
model = MyModel
fields = ('pk', 'id', 'name', ...)
default_columns = ('pk', 'name', ...)
```
### Table Configuration
The NetBoxTable class features dynamic configuration to allow users to change their column display and ordering preferences. To configure a table for a specific request, simply call its `configure()` method and pass the current HTTPRequest object. For example:
```python
table = MyModelTable(data=MyModel.objects.all())
table.configure(request)
```
This will automatically apply any user-specific preferences for the table. (If using a generic view provided by NetBox, table configuration is handled automatically.)
## Columns
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
::: netbox.tables.BooleanColumn
selection:
members: false
::: netbox.tables.ChoiceFieldColumn
selection:
members: false
::: netbox.tables.ColorColumn
selection:
members: false
::: netbox.tables.ColoredLabelColumn
selection:
members: false
::: netbox.tables.ContentTypeColumn
selection:
members: false
::: netbox.tables.ContentTypesColumn
selection:
members: false
::: netbox.tables.MarkdownColumn
selection:
members: false
::: netbox.tables.TagColumn
selection:
members: false
::: netbox.tables.TemplateColumn
selection:
members: false

View File

@ -0,0 +1,247 @@
# Templates
Templates are used to render HTML content generated from a set of context data. NetBox provides a set of built-in templates suitable for use in plugin views. Plugin authors can extend these templates to minimize the work needed to create custom templates while ensuring that the content they produce matches NetBox's layout and style. These templates are all written in the [Django Template Language (DTL)](https://docs.djangoproject.com/en/stable/ref/templates/language/).
## Template File Structure
Plugin templates should live in the `templates/<plugin-name>/` path within the plugin root. For example if your plugin's name is `my_plugin` and you create a template named `foo.html`, it should be saved to `templates/my_plugin/foo.html`. (You can of course use subdirectories below this point as well.) This ensures that Django's template engine can locate the template for rendering.
## Standard Blocks
The following template blocks are available on all templates.
| Name | Required | Description |
|----------------|----------|---------------------------------------------------------------------|
| `title` | Yes | Page title |
| `content` | Yes | Page content |
| `head` | - | Content to include in the HTML `<head>` element |
| `footer` | - | Page footer content |
| `footer_links` | - | Links section of the page footer |
| `javascript` | - | Javascript content included at the end of the HTML `<body>` element |
!!! note
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
## Base Templates
### layout.html
Path: `base/layout.html`
NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This is a general-purpose template that can be used when none of the function-specific templates below are suitable.
#### Blocks
| Name | Required | Description |
|-----------|----------|----------------------------|
| `header` | - | Page header |
| `tabs` | - | Horizontal navigation tabs |
| `modals` | - | Bootstrap 5 modal elements |
#### Example
An example of a plugin template which extends `layout.html` is included below.
```jinja2
{% extends 'base/layout.html' %}
{% block header %}
<h1>My Custom Header</h1>
{% endblock header %}
{% block content %}
<p>{{ some_plugin_context_var }}</p>
{% endblock content %}
```
The first line of the template instructs Django to extend the NetBox base template, and the `block` sections inject our custom content within its `header` and `content` blocks.
!!! note
Django renders templates with its own custom [template language](https://docs.djangoproject.com/en/stable/topics/templates/#the-django-template-language). This is very similar to Jinja2, however there are some important distinctions of which authors should be aware. Be sure to familiarize yourself with Django's template language before attempting to create new templates.
## Generic View Templates
### object.html
Path: `generic/object.html`
This template is used by the `ObjectView` generic view to display a single object.
#### Blocks
| Name | Required | Description |
|---------------------|----------|----------------------------------------------|
| `breadcrumbs` | - | Breadcrumb list items (HTML `<li>` elements) |
| `object_identifier` | - | A unique identifier (string) for the object |
| `extra_controls` | - | Additional action buttons to display |
| `extra_tabs` | - | Additional tabs to include |
#### Context
| Name | Required | Description |
|----------|----------|----------------------------------|
| `object` | Yes | The object instance being viewed |
### object_edit.html
Path: `generic/object_edit.html`
This template is used by the `ObjectEditView` generic view to create or modify a single object.
#### Blocks
| Name | Required | Description |
|------------------|----------|-------------------------------------------------------|
| `form` | - | Custom form content (within the HTML `<form>` element |
| `buttons` | - | Form submission buttons |
#### Context
| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `object` | Yes | The object instance being modified (or none, if creating) |
| `form` | Yes | The form class for creating/modifying the object |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |
### object_delete.html
Path: `generic/object_delete.html`
This template is used by the `ObjectDeleteView` generic view to delete a single object.
#### Blocks
None
#### Context
| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `object` | Yes | The object instance being deleted |
| `form` | Yes | The form class for confirming the object's deletion |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |
### object_list.html
Path: `generic/object_list.html`
This template is used by the `ObjectListView` generic view to display a filterable list of multiple objects.
#### Blocks
| Name | Required | Description |
|------------------|----------|--------------------------------------------------------------------|
| `extra_controls` | - | Additional action buttons |
| `bulk_buttons` | - | Additional bulk action buttons to display beneath the objects list |
#### Context
| Name | Required | Description |
|---------------|----------|---------------------------------------------------------------------------------------------|
| `model` | Yes | The object class |
| `table` | Yes | The table class used for rendering the list of objects |
| `permissions` | Yes | A mapping of add, change, and delete permissions for the current user |
| `actions` | Yes | A list of buttons to display (`add`, `import`, `export`, `bulk_edit`, and/or `bulk_delete`) |
| `filter_form` | - | The bound filterset form for filtering the objects list |
| `return_url` | - | The return URL to pass when submitting a bulk operation form |
### bulk_import.html
Path: `generic/bulk_import.html`
This template is used by the `BulkImportView` generic view to import multiple objects at once from CSV data.
#### Blocks
None
#### Context
| Name | Required | Description |
|--------------|----------|--------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The CSV import form class |
| `return_url` | - | The return URL to pass when submitting a bulk operation form |
| `fields` | - | A dictionary of form fields, to display import options |
### bulk_edit.html
Path: `generic/bulk_edit.html`
This template is used by the `BulkEditView` generic view to modify multiple objects simultaneously.
#### Blocks
None
#### Context
| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The bulk edit form class |
| `table` | Yes | The table class used for rendering the list of objects |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |
### bulk_delete.html
Path: `generic/bulk_delete.html`
This template is used by the `BulkDeleteView` generic view to delete multiple objects simultaneously.
#### Blocks
| Name | Required | Description |
|-----------------|----------|---------------------------------------|
| `message_extra` | - | Supplementary warning message content |
#### Context
| Name | Required | Description |
|--------------|----------|-----------------------------------------------------------------|
| `model` | Yes | The object class |
| `form` | Yes | The bulk delete form class |
| `table` | Yes | The table class used for rendering the list of objects |
| `return_url` | Yes | The URL to which the user is redirect after submitting the form |
## Tags
The following custom template tags are available in NetBox.
!!! info
These are loaded automatically by the template backend: You do _not_ need to include a `{% load %}` tag in your template to activate them.
::: utilities.templatetags.builtins.tags.badge
::: utilities.templatetags.builtins.tags.checkmark
::: utilities.templatetags.builtins.tags.tag
## Filters
The following custom template filters are available in NetBox.
!!! info
These are loaded automatically by the template backend: You do _not_ need to include a `{% load %}` tag in your template to activate them.
::: utilities.templatetags.builtins.filters.bettertitle
::: utilities.templatetags.builtins.filters.content_type
::: utilities.templatetags.builtins.filters.content_type_id
::: utilities.templatetags.builtins.filters.linkify
::: utilities.templatetags.builtins.filters.meta
::: utilities.templatetags.builtins.filters.placeholder
::: utilities.templatetags.builtins.filters.render_json
::: utilities.templatetags.builtins.filters.render_markdown
::: utilities.templatetags.builtins.filters.render_yaml
::: utilities.templatetags.builtins.filters.split
::: utilities.templatetags.builtins.filters.tzoffset

View File

@ -0,0 +1,177 @@
# Views
## Writing Views
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
As an example, let's write a view which displays a random animal and the sound it makes. We'll use Django's generic `View` class to minimize the amount of boilerplate code needed.
```python
from django.shortcuts import render
from django.views.generic import View
from .models import Animal
class RandomAnimalView(View):
"""
Display a randomly-selected animal.
"""
def get(self, request):
animal = Animal.objects.order_by('?').first()
return render(request, 'netbox_animal_sounds/animal.html', {
'animal': animal,
})
```
This view retrieves a random Animal instance from the database and passes it as a context variable when rendering a template named `animal.html`. HTTP `GET` requests are handled by the view's `get()` method, and `POST` requests are handled by its `post()` method.
Our example above is extremely simple, but views can do just about anything. They are generally where the core of your plugin's functionality will reside. Views also are not limited to returning HTML content: A view could return a CSV file or image, for instance. For more information on views, see the [Django documentation](https://docs.djangoproject.com/en/stable/topics/class-based-views/).
### URL Registration
To make the view accessible to users, we need to register a URL for it. We do this in `urls.py` by defining a `urlpatterns` variable containing a list of paths.
```python
from django.urls import path
from . import views
urlpatterns = [
path('random/', views.RandomAnimalView.as_view(), name='random_animal'),
]
```
A URL pattern has three components:
* `route` - The unique portion of the URL dedicated to this view
* `view` - The view itself
* `name` - A short name used to identify the URL path internally
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
### View Classes
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
| View Class | Description |
|--------------------|--------------------------------|
| `ObjectView` | View a single object |
| `ObjectEditView` | Create or edit a single object |
| `ObjectDeleteView` | Delete a single object |
| `ObjectListView` | View a list of objects |
| `BulkImportView` | Import a set of new objects |
| `BulkEditView` | Edit multiple objects |
| `BulkDeleteView` | Delete multiple objects |
!!! warning
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
#### Example Usage
```python
# views.py
from netbox.views.generic import ObjectEditView
from .models import Thing
class ThingEditView(ObjectEditView):
queryset = Thing.objects.all()
template_name = 'myplugin/thing.html'
...
```
## Object Views
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseObjectView
::: netbox.views.generic.ObjectView
selection:
members:
- get_object
- get_template_name
::: netbox.views.generic.ObjectEditView
selection:
members:
- get_object
- alter_object
::: netbox.views.generic.ObjectDeleteView
selection:
members:
- get_object
## Multi-Object Views
Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseMultiObjectView
::: netbox.views.generic.ObjectListView
selection:
members:
- get_table
- export_table
- export_template
::: netbox.views.generic.BulkImportView
selection:
members: false
::: netbox.views.generic.BulkEditView
selection:
members: false
::: netbox.views.generic.BulkDeleteView
selection:
members:
- get_form
## Feature Views
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView
selection:
members:
- get_form
::: netbox.views.generic.ObjectJournalView
selection:
members:
- get_form
## Extending Core Views
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
* `left_page()` - Inject content on the left side of the page
* `right_page()` - Inject content on the right side of the page
* `full_width_page()` - Inject content across the entire bottom of the page
* `buttons()` - Add buttons to the top of the page
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
* `object` - The object being viewed
* `request` - The current request
* `settings` - Global NetBox settings
* `config` - Plugin-specific configuration parameters
For example, accessing `{{ request.user }}` within a template will return the current user.
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
```python
from extras.plugins import PluginTemplateExtension
from .models import Animal
class SiteAnimalCount(PluginTemplateExtension):
model = 'dcim.site'
def right_page(self):
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
'animal_count': Animal.objects.count(),
})
template_extensions = [SiteAnimalCount]
```

View File

@ -10,6 +10,18 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release. This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 3.2](./version-3.2.md) (Pending Release)
* Plugins Framework Extensions ([#8333](https://github.com/netbox-community/netbox/issues/8333))
* Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
* Custom Object Fields ([#7006](https://github.com/netbox-community/netbox/issues/7006))
* Custom Status Choices ([#8054](https://github.com/netbox-community/netbox/issues/8054))
* Improved User Preferences ([#7759](https://github.com/netbox-community/netbox/issues/7759))
* Inventory Item Roles ([#3087](https://github.com/netbox-community/netbox/issues/3087))
* Inventory Item Templates ([#8118](https://github.com/netbox-community/netbox/issues/8118))
* Service Templates ([#1591](https://github.com/netbox-community/netbox/issues/1591))
* Automatic Provisioning of Next Available VLANs ([#2658](https://github.com/netbox-community/netbox/issues/2658))
#### [Version 3.1](./version-3.1.md) (December 2021) #### [Version 3.1](./version-3.1.md) (December 2021)
* Contact Objects ([#1344](https://github.com/netbox-community/netbox/issues/1344)) * Contact Objects ([#1344](https://github.com/netbox-community/netbox/issues/1344))

View File

@ -0,0 +1,229 @@
# NetBox v3.2
## v3.2.0 (FUTURE)
!!! warning "Python 3.8 or Later Required"
NetBox v3.2 requires Python 3.8 or later.
!!! warning "Deletion of Legacy Data"
This release includes a database migration that will remove the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields from the site model. (These fields have been superseded by the ASN and contact models introduced in NetBox v3.1.) To protect against the accidental destruction of data, the upgrade process **will fail** if any sites still have data in any of these fields. To bypass this safeguard, set the `NETBOX_DELETE_LEGACY_DATA` environment variable when running the upgrade script, which will permit the destruction of legacy data.
!!! tip "Migration Scripts"
A set of [migration scripts](https://github.com/netbox-community/migration-scripts) is available to assist with the migration of legacy site data.
### Breaking Changes
* Automatic redirection of legacy slug-based URL paths has been removed. URL-based slugs were changed to use numeric IDs in v2.11.0.
* The `asn` field has been removed from the site model. Please replicate any site ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
* The `asn` query filter for sites now matches against the AS number of assigned ASN objects.
* The `contact_name`, `contact_phone`, and `contact_email` fields have been removed from the site model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
* The `created` field of all change-logged models now conveys a full datetime object, rather than only a date. (Previous date-only values will receive a timestamp of 00:00.) While this change is largely unconcerning, strictly-typed API consumers may need to be updated.
* A `pre_run()` method has been added to the base Report class. Although unlikely to affect most installations, you may need to alter any reports which already use this name for a method.
* Webhook URLs now support Jinja2 templating. Although this is unlikely to introduce any issues, it's possible that an unusual URL might trigger a Jinja2 rendering error, in which case the URL would need to be properly escaped.
### New Features
#### Plugins Framework Extensions ([#8333](https://github.com/netbox-community/netbox/issues/8333))
NetBox's plugins framework has been extended considerably in this release. Additions include:
* Officially-supported generic view classes for common CRUD operations:
* `ObjectView`
* `ObjectEditView`
* `ObjectDeleteView`
* `ObjectListView`
* `BulkImportView`
* `BulkEditView`
* `BulkDeleteView`
* The `NetBoxModel` base class, which enables various NetBox features, including:
* Change logging
* Custom fields
* Custom links
* Custom validation
* Export templates
* Journaling
* Tags
* Webhooks
* Four base form classes for manipulating objects via the UI:
* `NetBoxModelForm`
* `NetBoxModelCSVForm`
* `NetBoxModelBulkEditForm`
* `NetBoxModelFilterSetForm`
* The `NetBoxModelFilterSet` base class for plugin filter sets
* The `NetBoxTable` base class for rendering object tables with `django-tables2`, as well as various custom column classes
* Function-specific templates (for generic views)
* Various custom template tags and filters
* `NetBoxModelViewSet` and several base serializer classes now provide enhanced REST API functionality
* Plugins can now extend NetBox's GraphQL API with their own schema
No breaking changes to previously supported components have been introduced in this release. However, plugin authors are encouraged to audit their existing code for misuse of unsupported components, as much of NetBox's internal code base has been reorganized.
#### Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
Several new models have been added to represent field-replaceable device modules, such as line cards installed within a chassis-based switch or router. Similar to devices, each module is instantiated from a user-defined module type, and can have components (interfaces, console ports, etc.) associated with it. These components become available to the parent device once the module has been installed within a module bay. This provides a convenient mechanism to effect the addition and deletion of device components as modules are installed and removed.
Automatic renaming of module components is also supported. When a new module is created, any occurrence of the string `{module}` in a component name will be replaced with the position of the module bay into which the module is being installed.
As with device types, the NetBox community offers a selection of curated real-world module type definitions in our [device type library](https://github.com/netbox-community/devicetype-library). These YAML files can be imported directly to NetBox for your convenience.
#### Custom Object Fields ([#7006](https://github.com/netbox-community/netbox/issues/7006))
Two new types of custom field have been introduced: object and multi-object. These can be used to associate an object in NetBox with some other arbitrary object(s) regardless of its type. For example, you might create a custom field named `primary_site` on the tenant model so that each tenant can have particular site designated as its primary. The multi-object custom field type allows for the assignment of multiple objects of the same type.
Custom field object assignment is fully supported in the REST API, and functions similarly to built-in foreign key relations. Nested representations are provided automatically for each custom field object.
#### Custom Status Choices ([#8054](https://github.com/netbox-community/netbox/issues/8054))
Custom choices can be now added to most object status fields in NetBox. This is done by defining the [`FIELD_CHOICES`](../configuration/optional-settings.md#field_choices) configuration parameter to map field identifiers to an iterable of custom choices an (optionally) colors. These choices are populated automatically when NetBox initializes. For example, the following configuration will add three custom choices for the site status field, each with a designated color:
```python
FIELD_CHOICES = {
'dcim.Site.status': (
('foo', 'Foo', 'red'),
('bar', 'Bar', 'green'),
('baz', 'Baz', 'blue'),
)
}
```
This will replace all default choices for this field with those listed. If instead the intent is to _extend_ the set of default choices, this can be done by appending a plus sign (`+`) to the end of the field identifier. For example, the following will add a single extra choice while retaining the defaults provided by NetBox:
```python
FIELD_CHOICES = {
'dcim.Site.status+': (
('fubar', 'FUBAR', 'red'),
)
}
```
#### Improved User Preferences ([#7759](https://github.com/netbox-community/netbox/issues/7759))
A robust new mechanism for managing user preferences is included in this release. The user preferences form has been improved for better usability, and administrators can now define default preferences for all users with the [`DEFAULT_USER_PREFERENCES`](../configuration/dynamic-settings.md##default_user_preferences) configuration parameter. For example, this can be used to define the columns which appear by default in a table:
```python
DEFAULT_USER_PREFERENCES = {
'tables': {
'IPAddressTable': {
'columns': ['address', 'status', 'created', 'description']
}
}
}
```
Users can adjust their own preferences under their user profile. A complete list of supported preferences is available in NetBox's [developer documentation](../development/user-preferences.md).
#### Inventory Item Roles ([#3087](https://github.com/netbox-community/netbox/issues/3087))
A new model has been introduced to represent functional roles for inventory items, similar to device roles. The assignment of roles to inventory items is optional.
#### Inventory Item Templates ([#8118](https://github.com/netbox-community/netbox/issues/8118))
Inventory items can now be templatized on a device type similar to other components (such as interfaces or console ports). This enables users to better pre-model fixed hardware components such as power supplies or hard disks.
Inventory item templates can be arranged hierarchically within a device type, and may be assigned to other templated components. These relationships will be mirrored when instantiating inventory items on a newly-created device (see [#7846](https://github.com/netbox-community/netbox/issues/7846)). For example, if defining an optic assigned to an interface template on a device type, the instantiated device will mimic this relationship between the optic and interface.
#### Service Templates ([#1591](https://github.com/netbox-community/netbox/issues/1591))
A new service template model has been introduced to assist in standardizing the definition and association of applications with devices and virtual machines. As an alternative to manually defining a name, protocol, and port(s) each time a service is created, a user now has the option of selecting a pre-defined template from which these values will be populated.
#### Automatic Provisioning of Next Available VLANs ([#2658](https://github.com/netbox-community/netbox/issues/2658))
A new REST API endpoint has been added at `/api/ipam/vlan-groups/<id>/available-vlans/`. A GET request to this endpoint will return a list of available VLANs within the group. A POST request can be made specifying the name(s) of one or more VLANs to create within the group, and their VLAN IDs will be assigned automatically from the available pool.
Where it is desired to limit the range of available VLANs within a group, users can define a minimum and/or maximum VLAN ID per group (see [#8168](https://github.com/netbox-community/netbox/issues/8168)).
### Enhancements
* [#5429](https://github.com/netbox-community/netbox/issues/5429) - Enable toggling the placement of table pagination controls
* [#6954](https://github.com/netbox-community/netbox/issues/6954) - Remember users' table ordering preferences
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Expose `AUTH_PASSWORD_VALIDATORS` setting to enforce password validation for local accounts
* [#7679](https://github.com/netbox-community/netbox/issues/7679) - Add actions menu to all object tables
* [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks
* [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts
* [#7846](https://github.com/netbox-community/netbox/issues/7846) - Enable associating inventory items with device components
* [#7852](https://github.com/netbox-community/netbox/issues/7852) - Enable the assignment of interfaces to VRFs
* [#7853](https://github.com/netbox-community/netbox/issues/7853) - Add `speed` and `duplex` fields to device interface model
* [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
* [#8295](https://github.com/netbox-community/netbox/issues/8295) - Jinja2 rendering is now supported for webhook URLs
* [#8296](https://github.com/netbox-community/netbox/issues/8296) - Allow disabling custom links
* [#8307](https://github.com/netbox-community/netbox/issues/8307) - Add `data_type` indicator to REST API serializer for custom fields
* [#8463](https://github.com/netbox-community/netbox/issues/8463) - Change the `created` field on all change-logged models from date to datetime
* [#8496](https://github.com/netbox-community/netbox/issues/8496) - Enable assigning multiple ASNs to a provider
* [#8572](https://github.com/netbox-community/netbox/issues/8572) - Add a `pre_run()` method for reports
* [#8593](https://github.com/netbox-community/netbox/issues/8593) - Add a `link` field for contacts
* [#8649](https://github.com/netbox-community/netbox/issues/8649) - Enable customization of configuration module using `NETBOX_CONFIGURATION` environment variable
* [#9006](https://github.com/netbox-community/netbox/issues/9006) - Enable custom fields, custom links, and tags for journal entries
### Bug Fixes (From Beta2)
* [#8658](https://github.com/netbox-community/netbox/issues/8658) - Fix display of assigned components under inventory item lists
* [#8838](https://github.com/netbox-community/netbox/issues/8838) - Fix FieldError exception during global search
* [#8845](https://github.com/netbox-community/netbox/issues/8845) - Correct default ASN formatting in table
* [#8869](https://github.com/netbox-community/netbox/issues/8869) - Fix NoReverseMatch exception when displaying tag w/assignments
* [#8872](https://github.com/netbox-community/netbox/issues/8872) - Enable filtering by custom object fields
* [#8970](https://github.com/netbox-community/netbox/issues/8970) - Permit nested inventory item templates on device types
* [#8976](https://github.com/netbox-community/netbox/issues/8976) - Add missing `object_type` field on CustomField REST API serializer
* [#8978](https://github.com/netbox-community/netbox/issues/8978) - Fix instantiation of front ports when provisioning a module
* [#9007](https://github.com/netbox-community/netbox/issues/9007) - Fix FieldError exception when instantiating a device type with nested inventory items
### Other Changes
* [#7731](https://github.com/netbox-community/netbox/issues/7731) - Require Python 3.8 or later
* [#7743](https://github.com/netbox-community/netbox/issues/7743) - Remove legacy ASN field from site model
* [#7748](https://github.com/netbox-community/netbox/issues/7748) - Remove legacy contact fields from site model
* [#8031](https://github.com/netbox-community/netbox/issues/8031) - Remove automatic redirection of legacy slug-based URLs
* [#8195](https://github.com/netbox-community/netbox/issues/8195), [#8454](https://github.com/netbox-community/netbox/issues/8454) - Use 64-bit integers for all primary keys
* [#8509](https://github.com/netbox-community/netbox/issues/8509) - `CSRF_TRUSTED_ORIGINS` is now a discrete configuration parameter (rather than being populated from `ALLOWED_HOSTS`)
* [#8684](https://github.com/netbox-community/netbox/issues/8684) - Change custom link template context variable `obj` to `object` (backward-compatible)
### REST API Changes
* Added the following endpoints:
* `/api/dcim/inventory-item-roles/`
* `/api/dcim/inventory-item-templates/`
* `/api/dcim/modules/`
* `/api/dcim/module-bays/`
* `/api/dcim/module-bay-templates/`
* `/api/dcim/module-types/`
* `/api/ipam/service-templates/`
* `/api/ipam/vlan-groups/<id>/available-vlans/`
* circuits.Provider
* Added `asns` field
* circuits.ProviderNetwork
* Added `service_id` field
* dcim.ConsolePort
* Added `module` field
* dcim.ConsoleServerPort
* Added `module` field
* dcim.FrontPort
* Added `module` field
* dcim.Interface
* Added `module`, `speed`, `duplex`, and `vrf` fields
* dcim.InventoryItem
* Added `component_type`, `component_id`, and `role` fields
* Added read-only `component` field (GFK)
* dcim.PowerPort
* Added `module` field
* dcim.PowerOutlet
* Added `module` field
* dcim.RearPort
* Added `module` field
* dcim.Site
* Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
* extras.ConfigContext
* Add `cluster_types` field
* extras.CustomField
* Added `data_type` and `object_type` fields
* extras.CustomLink
* Added `enabled` field
* extras.JournalEntry
* Added `custom_fields` and `tags` fields
* ipam.ASN
* Added `provider_count` field
* ipam.VLANGroup
* Added the `/availables-vlans/` endpoint
* Added `min_vid` and `max_vid` fields
* tenancy.Contact
* Added `link` field
* virtualization.VMInterface
* Added `vrf` field

View File

@ -1,7 +0,0 @@
# File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include
markdown-include
# MkDocs Material theme (for documentation build)
# https://github.com/squidfunk/mkdocs-material
mkdocs-material

View File

@ -18,6 +18,23 @@ theme:
toggle: toggle:
icon: material/lightbulb icon: material/lightbulb
name: Switch to Light Mode name: Switch to Light Mode
plugins:
- mkdocstrings:
handlers:
python:
setup_commands:
- import os
- import django
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
rendering:
heading_level: 3
members_order: source
show_root_heading: true
show_root_full_path: false
show_root_toc_entry: false
show_source: false
extra: extra:
social: social:
- icon: fontawesome/brands/github - icon: fontawesome/brands/github
@ -62,6 +79,7 @@ nav:
- Sites and Racks: 'core-functionality/sites-and-racks.md' - Sites and Racks: 'core-functionality/sites-and-racks.md'
- Devices and Cabling: 'core-functionality/devices.md' - Devices and Cabling: 'core-functionality/devices.md'
- Device Types: 'core-functionality/device-types.md' - Device Types: 'core-functionality/device-types.md'
- Modules: 'core-functionality/modules.md'
- Virtualization: 'core-functionality/virtualization.md' - Virtualization: 'core-functionality/virtualization.md'
- Service Mapping: 'core-functionality/services.md' - Service Mapping: 'core-functionality/services.md'
- Circuits: 'core-functionality/circuits.md' - Circuits: 'core-functionality/circuits.md'
@ -86,7 +104,18 @@ nav:
- Webhooks: 'additional-features/webhooks.md' - Webhooks: 'additional-features/webhooks.md'
- Plugins: - Plugins:
- Using Plugins: 'plugins/index.md' - Using Plugins: 'plugins/index.md'
- Developing Plugins: 'plugins/development.md' - Developing Plugins:
- Getting Started: 'plugins/development/index.md'
- Models: 'plugins/development/models.md'
- Views: 'plugins/development/views.md'
- Navigation: 'plugins/development/navigation.md'
- Templates: 'plugins/development/templates.md'
- Tables: 'plugins/development/tables.md'
- Forms: 'plugins/development/forms.md'
- Filters & Filter Sets: 'plugins/development/filtersets.md'
- REST API: 'plugins/development/rest-api.md'
- GraphQL API: 'plugins/development/graphql-api.md'
- Background Tasks: 'plugins/development/background-tasks.md'
- Administration: - Administration:
- Authentication: 'administration/authentication.md' - Authentication: 'administration/authentication.md'
- Permissions: 'administration/permissions.md' - Permissions: 'administration/permissions.md'
@ -115,6 +144,7 @@ nav:
- Release Checklist: 'development/release-checklist.md' - Release Checklist: 'development/release-checklist.md'
- Release Notes: - Release Notes:
- Summary: 'release-notes/index.md' - Summary: 'release-notes/index.md'
- Version 3.2: 'release-notes/version-3.2.md'
- Version 3.1: 'release-notes/version-3.1.md' - Version 3.1: 'release-notes/version-3.1.md'
- Version 3.0: 'release-notes/version-3.0.md' - Version 3.0: 'release-notes/version-3.0.md'
- Version 2.11: 'release-notes/version-2.11.md' - Version 2.11: 'release-notes/version-2.11.md'

View File

@ -4,8 +4,10 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import LinkTerminationSerializer from dcim.api.serializers import LinkTerminationSerializer
from netbox.api import ChoiceField from ipam.models import ASN
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer from ipam.api.nested_serializers import NestedASNSerializer
from netbox.api import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
@ -14,15 +16,23 @@ from .nested_serializers import *
# Providers # Providers
# #
class ProviderSerializer(PrimaryModelSerializer): class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,
required=False,
many=True
)
# Related object counts
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
] ]
@ -30,15 +40,15 @@ class ProviderSerializer(PrimaryModelSerializer):
# Provider networks # Provider networks
# #
class ProviderNetworkSerializer(PrimaryModelSerializer): class ProviderNetworkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = [ fields = [
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created', 'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
@ -46,7 +56,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
# Circuits # Circuits
# #
class CircuitTypeSerializer(PrimaryModelSerializer): class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
@ -70,7 +80,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
] ]
class CircuitSerializer(PrimaryModelSerializer): class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)

View File

@ -1,8 +1,8 @@
from netbox.api import OrderedDefaultRouter from netbox.api import NetBoxRouter
from . import views from . import views
router = OrderedDefaultRouter() router = NetBoxRouter()
router.APIRootView = views.CircuitsRootView router.APIRootView = views.CircuitsRootView
# Providers # Providers

View File

@ -3,8 +3,7 @@ from rest_framework.routers import APIRootView
from circuits import filtersets from circuits import filtersets
from circuits.models import * from circuits.models import *
from dcim.api.views import PassThroughPortMixin from dcim.api.views import PassThroughPortMixin
from extras.api.views import CustomFieldModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.views import ModelViewSet
from utilities.utils import count_related from utilities.utils import count_related
from . import serializers from . import serializers
@ -21,8 +20,8 @@ class CircuitsRootView(APIRootView):
# Providers # Providers
# #
class ProviderViewSet(CustomFieldModelViewSet): class ProviderViewSet(NetBoxModelViewSet):
queryset = Provider.objects.prefetch_related('tags').annotate( queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
circuit_count=count_related(Circuit, 'provider') circuit_count=count_related(Circuit, 'provider')
) )
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
@ -33,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
# Circuit Types # Circuit Types
# #
class CircuitTypeViewSet(CustomFieldModelViewSet): class CircuitTypeViewSet(NetBoxModelViewSet):
queryset = CircuitType.objects.prefetch_related('tags').annotate( queryset = CircuitType.objects.prefetch_related('tags').annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
) )
@ -45,7 +44,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
# Circuits # Circuits
# #
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(NetBoxModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider', 'termination_a', 'termination_z' 'type', 'tenant', 'provider', 'termination_a', 'termination_z'
).prefetch_related('tags') ).prefetch_related('tags')
@ -57,7 +56,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
# Circuit Terminations # Circuit Terminations
# #
class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', 'provider_network', 'cable' 'circuit', 'site', 'provider_network', 'cable'
) )
@ -70,7 +69,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
# Provider networks # Provider networks
# #
class ProviderNetworkViewSet(CustomFieldModelViewSet): class ProviderNetworkViewSet(NetBoxModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags') queryset = ProviderNetwork.objects.prefetch_related('tags')
serializer_class = serializers.ProviderNetworkSerializer serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet filterset_class = filtersets.ProviderNetworkFilterSet

View File

@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
# #
class CircuitStatusChoices(ChoiceSet): class CircuitStatusChoices(ChoiceSet):
key = 'Circuit.status'
STATUS_DEPROVISIONING = 'deprovisioning' STATUS_DEPROVISIONING = 'deprovisioning'
STATUS_ACTIVE = 'active' STATUS_ACTIVE = 'active'
@ -14,23 +15,14 @@ class CircuitStatusChoices(ChoiceSet):
STATUS_OFFLINE = 'offline' STATUS_OFFLINE = 'offline'
STATUS_DECOMMISSIONED = 'decommissioned' STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = ( CHOICES = [
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_PROVISIONING, 'Provisioning'), (STATUS_PROVISIONING, 'Provisioning', 'blue'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'green'),
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'red'),
(STATUS_DEPROVISIONING, 'Deprovisioning'), (STATUS_DEPROVISIONING, 'Deprovisioning', 'yellow'),
(STATUS_DECOMMISSIONED, 'Decommissioned'), (STATUS_DECOMMISSIONED, 'Decommissioned', 'gray'),
) ]
CSS_CLASSES = {
STATUS_DEPROVISIONING: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_PROVISIONING: 'primary',
STATUS_OFFLINE: 'danger',
STATUS_DECOMMISSIONED: 'secondary',
}
# #

View File

@ -3,8 +3,8 @@ from django.db.models import Q
from dcim.filtersets import CableTerminationFilterSet from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from extras.filters import TagFilter from ipam.models import ASN
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
@ -19,11 +19,7 @@ __all__ = (
) )
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='circuits__terminations__site__region', field_name='circuits__terminations__site__region',
@ -61,7 +57,11 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
tag = TagFilter() asn_id = django_filters.ModelMultipleChoiceFilter(
field_name='asns',
queryset=ASN.objects.all(),
label='ASN (ID)',
)
class Meta: class Meta:
model = Provider model = Provider
@ -79,11 +79,7 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
) )
class ProviderNetworkFilterSet(PrimaryModelFilterSet): class ProviderNetworkFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider (ID)', label='Provider (ID)',
@ -94,35 +90,30 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Provider (slug)', label='Provider (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = ['id', 'name', 'description'] fields = ['id', 'name', 'service_id', 'description']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(service_id__icontains=value) |
Q(description__icontains=value) | Q(description__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
).distinct() ).distinct()
class CircuitTypeFilterSet(OrganizationalModelFilterSet): class CircuitTypeFilterSet(OrganizationalModelFilterSet):
tag = TagFilter()
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider (ID)', label='Provider (ID)',
@ -189,7 +180,6 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = Circuit model = Circuit

View File

@ -1,10 +1,15 @@
from django import forms from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect from utilities.forms import (
add_blank_choice, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
StaticSelect,
)
__all__ = ( __all__ = (
'CircuitBulkEditForm', 'CircuitBulkEditForm',
@ -14,14 +19,15 @@ __all__ = (
) )
class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): class ProviderBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Provider.objects.all(),
widget=forms.MultipleHiddenInput
)
asn = forms.IntegerField( asn = forms.IntegerField(
required=False, required=False,
label='ASN' label='ASN (legacy)'
)
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
) )
account = forms.CharField( account = forms.CharField(
max_length=30, max_length=30,
@ -47,23 +53,27 @@ class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label='Comments' label='Comments'
) )
class Meta: model = Provider
nullable_fields = [ fieldsets = (
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', (None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
]
class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = (
'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
)
class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False required=False
) )
description = forms.CharField( service_id = forms.CharField(
max_length=100, max_length=100,
required=False,
label='Service ID'
)
description = forms.CharField(
max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField(
@ -71,31 +81,29 @@ class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor
label='Comments' label='Comments'
) )
class Meta: model = ProviderNetwork
nullable_fields = [ fieldsets = (
'description', 'comments', (None, ('provider', 'service_id', 'description')),
]
class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = (
'service_id', 'description', 'comments',
)
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
) )
class Meta: model = CircuitType
nullable_fields = ['description'] fieldsets = (
(None, ('description',)),
class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Circuit.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('description',)
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False required=False
@ -127,7 +135,10 @@ class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label='Comments' label='Comments'
) )
class Meta: model = Circuit
nullable_fields = [ fieldsets = (
'tenant', 'commit_rate', 'description', 'comments', (None, ('type', 'provider', 'status', 'tenant', 'commit_rate', 'description')),
] )
nullable_fields = (
'tenant', 'commit_rate', 'description', 'comments',
)

View File

@ -1,6 +1,6 @@
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from extras.forms import CustomFieldModelCSVForm from netbox.forms import NetBoxModelCSVForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
@ -12,7 +12,7 @@ __all__ = (
) )
class ProviderCSVForm(CustomFieldModelCSVForm): class ProviderCSVForm(NetBoxModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -22,7 +22,7 @@ class ProviderCSVForm(CustomFieldModelCSVForm):
) )
class ProviderNetworkCSVForm(CustomFieldModelCSVForm): class ProviderNetworkCSVForm(NetBoxModelCSVForm):
provider = CSVModelChoiceField( provider = CSVModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
@ -32,11 +32,11 @@ class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = [ fields = [
'provider', 'name', 'description', 'comments', 'provider', 'name', 'service_id', 'description', 'comments',
] ]
class CircuitTypeCSVForm(CustomFieldModelCSVForm): class CircuitTypeCSVForm(NetBoxModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -47,7 +47,7 @@ class CircuitTypeCSVForm(CustomFieldModelCSVForm):
} }
class CircuitCSVForm(CustomFieldModelCSVForm): class CircuitCSVForm(NetBoxModelCSVForm):
provider = CSVModelChoiceField( provider = CSVModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',

View File

@ -4,9 +4,10 @@ from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
__all__ = ( __all__ = (
'CircuitFilterForm', 'CircuitFilterForm',
@ -16,14 +17,14 @@ __all__ = (
) )
class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider model = Provider
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_group_id', 'site_id'], ('Location', ('region_id', 'site_group_id', 'site_id')),
['asn'], ('ASN', ('asn',)),
['contact', 'contact_role'] ('Contacts', ('contact', 'contact_role')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -45,40 +46,49 @@ class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
) )
asn = forms.IntegerField( asn = forms.IntegerField(
required=False, required=False,
label=_('ASN') label=_('ASN (legacy)')
)
asn_id = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
required=False,
label=_('ASNs')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
class ProviderNetworkFilterForm(CustomFieldModelFilterForm): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork model = ProviderNetwork
field_groups = ( fieldsets = (
('q', 'tag'), (None, ('q', 'tag')),
('provider_id',), ('Attributes', ('provider_id', 'service_id')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False, required=False,
label=_('Provider') label=_('Provider')
) )
service_id = forms.CharField(
max_length=100,
required=False
)
tag = TagFilterField(model) tag = TagFilterField(model)
class CircuitTypeFilterForm(CustomFieldModelFilterForm): class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = CircuitType model = CircuitType
tag = TagFilterField(model) tag = TagFilterField(model)
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit model = Circuit
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['provider_id', 'provider_network_id'], ('Provider', ('provider_id', 'provider_network_id')),
['type_id', 'status', 'commit_rate'], ('Attributes', ('type_id', 'status', 'commit_rate')),
['region_id', 'site_group_id', 'site_id'], ('Location', ('region_id', 'site_group_id', 'site_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
['contact', 'contact_role'] ('Contacts', ('contact', 'contact_role')),
] )
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False, required=False,
@ -97,10 +107,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldMo
}, },
label=_('Provider network') label=_('Provider network')
) )
status = forms.MultipleChoiceField( status = MultipleChoiceField(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),

View File

@ -1,9 +1,10 @@
from django import forms from django import forms
from django.utils.translation import gettext as _
from circuits.models import * from circuits.models import *
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from extras.forms import CustomFieldModelForm from ipam.models import ASN
from extras.models import Tag from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms import ( from utilities.forms import (
BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@ -19,23 +20,25 @@ __all__ = (
) )
class ProviderForm(CustomFieldModelForm): class ProviderForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
comments = CommentField() asns = DynamicModelMultipleChoiceField(
tags = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(),
queryset=Tag.objects.all(), label=_('ASNs'),
required=False required=False
) )
comments = CommentField()
fieldsets = (
('Provider', ('name', 'slug', 'asn', 'asns', 'tags')),
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
)
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asns', 'comments', 'tags',
] ]
fieldsets = (
('Provider', ('name', 'slug', 'asn', 'tags')),
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
)
widgets = { widgets = {
'noc_contact': SmallTextarea( 'noc_contact': SmallTextarea(
attrs={'rows': 5} attrs={'rows': 5}
@ -53,32 +56,25 @@ class ProviderForm(CustomFieldModelForm):
} }
class ProviderNetworkForm(CustomFieldModelForm): class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all() queryset=Provider.objects.all()
) )
comments = CommentField() comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), fieldsets = (
required=False ('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')),
) )
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = [ fields = [
'provider', 'name', 'description', 'comments', 'tags', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
] ]
fieldsets = (
('Provider Network', ('provider', 'name', 'description', 'tags')),
)
class CircuitTypeForm(CustomFieldModelForm): class CircuitTypeForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = CircuitType model = CircuitType
@ -87,7 +83,7 @@ class CircuitTypeForm(CustomFieldModelForm):
] ]
class CircuitForm(TenancyForm, CustomFieldModelForm): class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all() queryset=Provider.objects.all()
) )
@ -95,9 +91,10 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
queryset=CircuitType.objects.all() queryset=CircuitType.objects.all()
) )
comments = CommentField() comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), fieldsets = (
required=False ('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -106,10 +103,6 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments', 'tags', 'comments', 'tags',
] ]
fieldsets = (
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
'commit_rate': "Committed rate", 'commit_rate': "Committed rate",
@ -122,6 +115,19 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False,
initial_params={
'circuits': '$circuit'
}
)
circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
query_params={
'provider_id': '$provider',
},
)
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -152,8 +158,8 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed', 'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected',
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
] ]
help_texts = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
@ -161,12 +167,7 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
'pp_info': "Patch panel ID and port number(s)" 'pp_info': "Patch panel ID and port number(s)"
} }
widgets = { widgets = {
'term_side': forms.HiddenInput(), 'term_side': StaticSelect(),
'port_speed': SelectSpeedWidget(), 'port_speed': SelectSpeedWidget(),
'upstream_speed': SelectSpeedWidget(), 'upstream_speed': SelectSpeedWidget(),
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)

View File

@ -1,5 +1,5 @@
from circuits import filtersets, models from circuits import filtersets, models
from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'CircuitTerminationType', 'CircuitTerminationType',
@ -18,7 +18,7 @@ class CircuitTerminationType(ObjectType):
filterset_class = filtersets.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
class CircuitType(PrimaryObjectType): class CircuitType(NetBoxObjectType):
class Meta: class Meta:
model = models.Circuit model = models.Circuit
@ -34,7 +34,7 @@ class CircuitTypeType(OrganizationalObjectType):
filterset_class = filtersets.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
class ProviderType(PrimaryObjectType): class ProviderType(NetBoxObjectType):
class Meta: class Meta:
model = models.Provider model = models.Provider
@ -42,7 +42,7 @@ class ProviderType(PrimaryObjectType):
filterset_class = filtersets.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
class ProviderNetworkType(PrimaryObjectType): class ProviderNetworkType(NetBoxObjectType):
class Meta: class Meta:
model = models.ProviderNetwork model = models.ProviderNetwork

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0004_rename_cable_peer'),
]
operations = [
migrations.AddField(
model_name='providernetwork',
name='service_id',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,44 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0032_provider_service_id'),
]
operations = [
# Model IDs
migrations.AlterField(
model_name='circuit',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittermination',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittype',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='provider',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='providernetwork',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
# GFK IDs
migrations.AlterField(
model_name='circuittermination',
name='_link_peer_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.0.2 on 2022-02-08 18:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0033_standardize_id_fields'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='circuittermination',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='circuittype',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='provider',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='providernetwork',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.0.3 on 2022-03-30 20:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0057_created_datetimefield'),
('circuits', '0034_created_datetimefield'),
]
operations = [
migrations.AddField(
model_name='provider',
name='asns',
field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'),
),
]

View File

@ -5,8 +5,8 @@ from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from dcim.models import LinkTermination from dcim.models import LinkTermination
from extras.utils import extras_features from netbox.models import ChangeLoggedModel, OrganizationalModel, NetBoxModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel from netbox.models.features import WebhooksMixin
__all__ = ( __all__ = (
'Circuit', 'Circuit',
@ -15,7 +15,6 @@ __all__ = (
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class CircuitType(OrganizationalModel): class CircuitType(OrganizationalModel):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
@ -44,8 +43,7 @@ class CircuitType(OrganizationalModel):
return reverse('circuits:circuittype', args=[self.pk]) return reverse('circuits:circuittype', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Circuit(NetBoxModel):
class Circuit(PrimaryModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@ -134,12 +132,11 @@ class Circuit(PrimaryModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk]) return reverse('circuits:circuit', args=[self.pk])
def get_status_class(self): def get_status_color(self):
return CircuitStatusChoices.CSS_CLASSES.get(self.status) return CircuitStatusChoices.colors.get(self.status)
@extras_features('webhooks') class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
class CircuitTermination(ChangeLoggedModel, LinkTermination):
circuit = models.ForeignKey( circuit = models.ForeignKey(
to='circuits.Circuit', to='circuits.Circuit',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -212,13 +209,9 @@ class CircuitTermination(ChangeLoggedModel, LinkTermination):
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.") raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
def to_objectchange(self, action): def to_objectchange(self, action):
# Annotate the parent Circuit objectchange = super().to_objectchange(action)
try: objectchange.related_object = self.circuit
circuit = self.circuit return objectchange
except Circuit.DoesNotExist:
# Parent circuit has been deleted
circuit = None
return super().to_objectchange(action, related_object=circuit)
@property @property
def parent_object(self): def parent_object(self):

View File

@ -3,9 +3,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.utils import extras_features from netbox.models import NetBoxModel
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet
__all__ = ( __all__ = (
'ProviderNetwork', 'ProviderNetwork',
@ -13,8 +11,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Provider(NetBoxModel):
class Provider(PrimaryModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@ -33,6 +30,11 @@ class Provider(PrimaryModel):
verbose_name='ASN', verbose_name='ASN',
help_text='32-bit autonomous system number' help_text='32-bit autonomous system number'
) )
asns = models.ManyToManyField(
to='ipam.ASN',
related_name='providers',
blank=True
)
account = models.CharField( account = models.CharField(
max_length=30, max_length=30,
blank=True, blank=True,
@ -73,8 +75,7 @@ class Provider(PrimaryModel):
return reverse('circuits:provider', args=[self.pk]) return reverse('circuits:provider', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ProviderNetwork(NetBoxModel):
class ProviderNetwork(PrimaryModel):
""" """
This represents a provider network which exists outside of NetBox, the details of which are unknown or This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user. unimportant to the user.
@ -87,6 +88,11 @@ class ProviderNetwork(PrimaryModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='networks' related_name='networks'
) )
service_id = models.CharField(
max_length=100,
blank=True,
verbose_name='Service ID'
)
description = models.CharField( description = models.CharField(
max_length=200, max_length=200,
blank=True blank=True

View File

@ -1,167 +0,0 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import TenantColumn
from utilities.tables import (
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
)
from .models import *
__all__ = (
'CircuitTable',
'CircuitTypeTable',
'ProviderTable',
'ProviderNetworkTable',
)
CIRCUITTERMINATION_LINK = """
{% if value.site %}
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
{% elif value.provider_network %}
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
{% endif %}
"""
#
# Table columns
#
class CommitRateColumn(tables.TemplateColumn):
"""
Humanize the commit rate in the column view
"""
template_code = """
{% load helpers %}
{{ record.commit_rate|humanize_speed }}
"""
def __init__(self, *args, **kwargs):
super().__init__(template_code=self.template_code, *args, **kwargs)
def value(self, value):
return str(value) if value else None
#
# Providers
#
class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
circuit_count = LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_id': 'pk'},
verbose_name='Circuits'
)
comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='circuits:provider_list'
)
class Meta(BaseTable.Meta):
model = Provider
fields = (
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
# Provider networks
#
class ProviderNetworkTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
provider = tables.Column(
linkify=True
)
comments = MarkdownColumn()
tags = TagColumn(
url_name='circuits:providernetwork_list'
)
class Meta(BaseTable.Meta):
model = ProviderNetwork
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'provider', 'description')
#
# Circuit types
#
class CircuitTypeTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='circuits:circuittype_list'
)
circuit_count = tables.Column(
verbose_name='Circuits'
)
actions = ButtonsColumn(CircuitType)
class Meta(BaseTable.Meta):
model = CircuitType
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
#
# Circuits
#
class CircuitTable(BaseTable):
pk = ToggleColumn()
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
)
provider = tables.Column(
linkify=True
)
status = ChoiceFieldColumn()
tenant = TenantColumn()
termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
)
termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side Z'
)
commit_rate = CommitRateColumn()
comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='circuits:circuit_list'
)
class Meta(BaseTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
)

View File

@ -0,0 +1,3 @@
from .circuits import *
from .columns import *
from .providers import *

View File

@ -0,0 +1,77 @@
import django_tables2 as tables
from circuits.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from .columns import CommitRateColumn
__all__ = (
'CircuitTable',
'CircuitTypeTable',
)
CIRCUITTERMINATION_LINK = """
{% if value.site %}
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
{% elif value.provider_network %}
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
{% endif %}
"""
class CircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True
)
tags = columns.TagColumn(
url_name='circuits:circuittype_list'
)
circuit_count = tables.Column(
verbose_name='Circuits'
)
class Meta(NetBoxTable.Meta):
model = CircuitType
fields = (
'pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
)
provider = tables.Column(
linkify=True
)
status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
)
termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side Z'
)
commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)
class Meta(NetBoxTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
)

View File

@ -0,0 +1,21 @@
import django_tables2 as tables
__all__ = (
'CommitRateColumn',
)
class CommitRateColumn(tables.TemplateColumn):
"""
Humanize the commit rate in the column view
"""
template_code = """
{% load helpers %}
{{ record.commit_rate|humanize_speed }}
"""
def __init__(self, *args, **kwargs):
super().__init__(template_code=self.template_code, *args, **kwargs)
def value(self, value):
return str(value) if value else None

View File

@ -0,0 +1,67 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from circuits.models import *
from netbox.tables import NetBoxTable, columns
__all__ = (
'ProviderTable',
'ProviderNetworkTable',
)
class ProviderTable(NetBoxTable):
name = tables.Column(
linkify=True
)
asns = tables.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
)
asn_count = columns.LinkedCountColumn(
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'provider_id': 'pk'},
verbose_name='ASN Count'
)
circuit_count = columns.LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_id': 'pk'},
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:provider_list'
)
class Meta(NetBoxTable.Meta):
model = Provider
fields = (
'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn_count',
'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
class ProviderNetworkTable(NetBoxTable):
name = tables.Column(
linkify=True
)
provider = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:providernetwork_list'
)
class Meta(NetBoxTable.Meta):
model = ProviderNetwork
fields = (
'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags',
)
default_columns = ('pk', 'name', 'provider', 'service_id', 'description')

View File

@ -3,6 +3,7 @@ from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import * from circuits.models import *
from dcim.models import Site from dcim.models import Site
from ipam.models import ASN, RIR
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -18,20 +19,6 @@ class AppTest(APITestCase):
class ProviderTest(APIViewTestCases.APIViewTestCase): class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider model = Provider
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [
{
'name': 'Provider 4',
'slug': 'provider-4',
},
{
'name': 'Provider 5',
'slug': 'provider-5',
},
{
'name': 'Provider 6',
'slug': 'provider-6',
},
]
bulk_update_data = { bulk_update_data = {
'asn': 1234, 'asn': 1234,
} }
@ -39,6 +26,12 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = [
ASN(asn=65000 + i, rir=rir) for i in range(8)
]
ASN.objects.bulk_create(asns)
providers = ( providers = (
Provider(name='Provider 1', slug='provider-1'), Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'), Provider(name='Provider 2', slug='provider-2'),
@ -46,6 +39,24 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
cls.create_data = [
{
'name': 'Provider 4',
'slug': 'provider-4',
'asns': [asns[0].pk, asns[1].pk],
},
{
'name': 'Provider 5',
'slug': 'provider-5',
'asns': [asns[2].pk, asns[3].pk],
},
{
'name': 'Provider 6',
'slug': 'provider-6',
'asns': [asns[4].pk, asns[5].pk],
},
]
class CircuitTypeTest(APIViewTestCases.APIViewTestCase): class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = CircuitType model = CircuitType

View File

@ -4,6 +4,7 @@ from circuits.choices import *
from circuits.filtersets import * from circuits.filtersets import *
from circuits.models import * from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup from dcim.models import Cable, Region, Site, SiteGroup
from ipam.models import ASN, RIR
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests from utilities.testing import ChangeLoggedFilterSetTests
@ -15,6 +16,14 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = (
ASN(asn=64512, rir=rir),
ASN(asn=64513, rir=rir),
ASN(asn=64514, rir=rir),
)
ASN.objects.bulk_create(asns)
providers = ( providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'), Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'), Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
@ -23,6 +32,9 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'), Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
providers[0].asns.set([asns[0]])
providers[1].asns.set([asns[1]])
providers[2].asns.set([asns[2]])
regions = ( regions = (
Region(name='Test Region 1', slug='test-region-1'), Region(name='Test Region 1', slug='test-region-1'),
@ -70,10 +82,15 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['provider-1', 'provider-2']} params = {'slug': ['provider-1', 'provider-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asn(self): def test_asn(self): # Legacy field
params = {'asn': ['65001', '65002']} params = {'asn': ['65001', '65002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asn_id(self): # ASN object assignment
asns = ASN.objects.all()[:2]
params = {'asn_id': [asns[0].pk, asns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_account(self): def test_account(self):
params = {'account': ['1234', '2345']} params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -6,6 +6,7 @@ from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import * from circuits.models import *
from dcim.models import Cable, Interface, Site from dcim.models import Cable, Interface, Site
from ipam.models import ASN, RIR
from utilities.testing import ViewTestCases, create_tags, create_test_device from utilities.testing import ViewTestCases, create_tags, create_test_device
@ -15,11 +16,21 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
Provider.objects.bulk_create([ rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = [
ASN(asn=65000 + i, rir=rir) for i in range(8)
]
ASN.objects.bulk_create(asns)
providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 1', slug='provider-1', asn=65001),
Provider(name='Provider 2', slug='provider-2', asn=65002), Provider(name='Provider 2', slug='provider-2', asn=65002),
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3', asn=65003),
]) )
Provider.objects.bulk_create(providers)
providers[0].asns.set([asns[0], asns[1]])
providers[1].asns.set([asns[2], asns[3]])
providers[2].asns.set([asns[4], asns[5]])
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
@ -27,6 +38,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'name': 'Provider X', 'name': 'Provider X',
'slug': 'provider-x', 'slug': 'provider-x',
'asn': 65123, 'asn': 65123,
'asns': [asns[6].pk, asns[7].pk],
'account': '1234', 'account': '1234',
'portal_url': 'http://example.com/portal', 'portal_url': 'http://example.com/portal',
'noc_contact': 'noc@example.com', 'noc_contact': 'noc@example.com',
@ -218,6 +230,7 @@ class CircuitTerminationTestCase(
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
cls.form_data = { cls.form_data = {
'circuit': circuits[2].pk,
'term_side': 'A', 'term_side': 'A',
'site': sites[2].pk, 'site': sites[2].pk,
'description': 'New description', 'description': 'New description',

View File

@ -1,8 +1,7 @@
from django.urls import path from django.urls import path
from dcim.views import CableCreateView, PathTraceView from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from utilities.views import SlugRedirectView
from . import views from . import views
from .models import * from .models import *
@ -16,7 +15,6 @@ urlpatterns = [
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'), path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'),
path('providers/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': Provider}),
path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
@ -59,7 +57,7 @@ urlpatterns = [
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
# Circuit terminations # Circuit terminations
path('circuits/<int:circuit>/terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),

View File

@ -5,10 +5,8 @@ from django.shortcuts import get_object_or_404, redirect, render
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.tables import paginate_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import CircuitTerminationSideChoices
from .models import * from .models import *
@ -35,7 +33,7 @@ class ProviderView(generic.ObjectView):
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
) )
circuits_table = tables.CircuitTable(circuits, exclude=('provider',)) circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
paginate_table(circuits_table, request) circuits_table.configure(request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -44,7 +42,7 @@ class ProviderView(generic.ObjectView):
class ProviderEditView(generic.ObjectEditView): class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
model_form = forms.ProviderForm form = forms.ProviderForm
class ProviderDeleteView(generic.ObjectDeleteView): class ProviderDeleteView(generic.ObjectDeleteView):
@ -96,7 +94,7 @@ class ProviderNetworkView(generic.ObjectView):
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
) )
circuits_table = tables.CircuitTable(circuits) circuits_table = tables.CircuitTable(circuits)
paginate_table(circuits_table, request) circuits_table.configure(request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -105,7 +103,7 @@ class ProviderNetworkView(generic.ObjectView):
class ProviderNetworkEditView(generic.ObjectEditView): class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all() queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkForm form = forms.ProviderNetworkForm
class ProviderNetworkDeleteView(generic.ObjectDeleteView): class ProviderNetworkDeleteView(generic.ObjectDeleteView):
@ -150,7 +148,7 @@ class CircuitTypeView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance) circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
circuits_table = tables.CircuitTable(circuits, exclude=('type',)) circuits_table = tables.CircuitTable(circuits, exclude=('type',))
paginate_table(circuits_table, request) circuits_table.configure(request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -159,7 +157,7 @@ class CircuitTypeView(generic.ObjectView):
class CircuitTypeEditView(generic.ObjectEditView): class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm form = forms.CircuitTypeForm
class CircuitTypeDeleteView(generic.ObjectDeleteView): class CircuitTypeDeleteView(generic.ObjectDeleteView):
@ -207,7 +205,7 @@ class CircuitView(generic.ObjectView):
class CircuitEditView(generic.ObjectEditView): class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
model_form = forms.CircuitForm form = forms.CircuitForm
class CircuitDeleteView(generic.ObjectDeleteView): class CircuitDeleteView(generic.ObjectDeleteView):
@ -317,17 +315,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
class CircuitTerminationEditView(generic.ObjectEditView): class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationForm form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html' template_name = 'circuits/circuittermination_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'circuit' in url_kwargs:
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
return obj
def get_return_url(self, request, obj):
return obj.circuit.get_absolute_url()
class CircuitTerminationDeleteView(generic.ObjectDeleteView): class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()

View File

@ -4,6 +4,7 @@ from dcim import models
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [ __all__ = [
'ComponentNestedModuleSerializer',
'NestedCableSerializer', 'NestedCableSerializer',
'NestedConsolePortSerializer', 'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer', 'NestedConsolePortTemplateSerializer',
@ -19,7 +20,13 @@ __all__ = [
'NestedInterfaceSerializer', 'NestedInterfaceSerializer',
'NestedInterfaceTemplateSerializer', 'NestedInterfaceTemplateSerializer',
'NestedInventoryItemSerializer', 'NestedInventoryItemSerializer',
'NestedInventoryItemRoleSerializer',
'NestedInventoryItemTemplateSerializer',
'NestedManufacturerSerializer', 'NestedManufacturerSerializer',
'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer',
'NestedModuleSerializer',
'NestedModuleTypeSerializer',
'NestedPlatformSerializer', 'NestedPlatformSerializer',
'NestedPowerFeedSerializer', 'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer', 'NestedPowerOutletSerializer',
@ -117,7 +124,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
# #
# Device types # Device/module types
# #
class NestedManufacturerSerializer(WritableNestedSerializer): class NestedManufacturerSerializer(WritableNestedSerializer):
@ -139,6 +146,20 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count'] fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
fields = ['id', 'url', 'display', 'manufacturer', 'model']
#
# Component templates
#
class NestedConsolePortTemplateSerializer(WritableNestedSerializer): class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
@ -195,6 +216,14 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
class Meta:
model = models.ModuleBayTemplate
fields = ['id', 'url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer): class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
@ -203,6 +232,15 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = models.InventoryItemTemplate
fields = ['id', 'url', 'display', 'name', '_depth']
# #
# Devices # Devices
# #
@ -235,6 +273,37 @@ class NestedDeviceSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'name']
class ComponentNestedModuleSerializer(WritableNestedSerializer):
"""
Used by device component serializers.
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'device', 'module_bay']
class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True)
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
module_type = NestedModuleTypeSerializer(read_only=True)
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'device', 'module_bay', 'module_type']
class NestedConsoleServerPortSerializer(WritableNestedSerializer): class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
@ -298,6 +367,15 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
module = NestedModuleSerializer(read_only=True)
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
@ -317,6 +395,15 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'device', 'name', '_depth'] fields = ['id', 'url', 'display', 'device', 'name', '_depth']
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.InventoryItemRole
fields = ['id', 'url', 'display', 'name', 'slug', 'inventoryitem_count']
# #
# Cables # Cables
# #

View File

@ -6,11 +6,13 @@ from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN from ipam.models import ASN, VLAN
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import ( from netbox.api.serializers import (
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
) )
from netbox.config import ConfigItem from netbox.config import ConfigItem
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@ -107,7 +109,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
] ]
class SiteSerializer(PrimaryModelSerializer): class SiteSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False) status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True) region = NestedRegionSerializer(required=False, allow_null=True)
@ -132,10 +134,10 @@ class SiteSerializer(PrimaryModelSerializer):
class Meta: class Meta:
model = Site model = Site
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns', 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', 'virtualmachine_count', 'vlan_count',
] ]
@ -159,7 +161,7 @@ class LocationSerializer(NestedGroupModelSerializer):
] ]
class RackRoleSerializer(PrimaryModelSerializer): class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
@ -171,7 +173,7 @@ class RackRoleSerializer(PrimaryModelSerializer):
] ]
class RackSerializer(PrimaryModelSerializer): class RackSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
location = NestedLocationSerializer(required=False, allow_null=True, default=None) location = NestedLocationSerializer(required=False, allow_null=True, default=None)
@ -210,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer):
return obj['name'] return obj['name']
class RackReservationSerializer(PrimaryModelSerializer): class RackReservationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer() rack = NestedRackSerializer()
user = NestedUserSerializer() user = NestedUserSerializer()
@ -261,10 +263,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# #
# Device types # Device/module types
# #
class ManufacturerSerializer(PrimaryModelSerializer): class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True) devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True)
@ -278,7 +280,7 @@ class ManufacturerSerializer(PrimaryModelSerializer):
] ]
class DeviceTypeSerializer(PrimaryModelSerializer): class DeviceTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@ -294,6 +296,23 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
] ]
class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = ModuleType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
#
# Component templates
#
class ConsolePortTemplateSerializer(ValidatedModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
@ -409,6 +428,18 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
] ]
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
device_type = NestedDeviceTypeSerializer()
class Meta:
model = ModuleBayTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
'last_updated',
]
class DeviceBayTemplateSerializer(ValidatedModelSerializer): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
@ -418,11 +449,45 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
device_type = NestedDeviceTypeSerializer()
parent = serializers.PrimaryKeyRelatedField(
queryset=InventoryItemTemplate.objects.all(),
allow_null=True,
default=None
)
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
component_type = ContentTypeField(
queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItemTemplate
fields = [
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_component(self, obj):
if obj.component is None:
return None
serializer = get_serializer_for_model(obj.component, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.component, context=context).data
# #
# Devices # Devices
# #
class DeviceRoleSerializer(PrimaryModelSerializer): class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True)
@ -435,7 +500,7 @@ class DeviceRoleSerializer(PrimaryModelSerializer):
] ]
class PlatformSerializer(PrimaryModelSerializer): class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -449,7 +514,7 @@ class PlatformSerializer(PrimaryModelSerializer):
] ]
class DeviceSerializer(PrimaryModelSerializer): class DeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer() device_role = NestedDeviceRoleSerializer()
@ -491,6 +556,20 @@ class DeviceSerializer(PrimaryModelSerializer):
return data return data
class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer()
module_bay = NestedModuleBaySerializer()
module_type = NestedModuleTypeSerializer()
class Meta:
model = Module
fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
class DeviceWithConfigContextSerializer(DeviceSerializer): class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField() config_context = serializers.SerializerMethodField()
@ -515,9 +594,13 @@ class DeviceNAPALMSerializer(serializers.Serializer):
# Device components # Device components
# #
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True, allow_blank=True,
@ -533,15 +616,19 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True, allow_blank=True,
@ -557,15 +644,19 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
allow_blank=True, allow_blank=True,
@ -587,15 +678,20 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
allow_blank=True, allow_blank=True,
@ -606,20 +702,26 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True) parent = NestedInterfaceSerializer(required=False, allow_null=True)
bridge = NestedInterfaceSerializer(required=False, allow_null=True) bridge = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@ -629,6 +731,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
required=False, required=False,
many=True many=True
) )
vrf = NestedVRFSerializer(required=False, allow_null=True)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
wireless_link = NestedWirelessLinkSerializer(read_only=True) wireless_link = NestedWirelessLinkSerializer(read_only=True)
wireless_lans = SerializedPKRelatedField( wireless_lans = SerializedPKRelatedField(
@ -643,12 +746,12 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint', 'connected_endpoint_type', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'count_fhrp_groups', '_occupied', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
] ]
def validate(self, data): def validate(self, data):
@ -665,16 +768,20 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
return super().validate(data) return super().validate(data)
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
@ -691,9 +798,13 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name', 'label'] fields = ['id', 'url', 'display', 'name', 'label']
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer() rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
@ -701,13 +812,26 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags',
'created', 'last_updated', '_occupied', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class DeviceBaySerializer(PrimaryModelSerializer): class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields',
'created', 'last_updated',
]
class DeviceBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True) installed_device = NestedDeviceSerializer(required=False, allow_null=True)
@ -720,22 +844,50 @@ class DeviceBaySerializer(PrimaryModelSerializer):
] ]
# class InventoryItemSerializer(NetBoxModelSerializer):
# Inventory items
#
class InventoryItemSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
component_type = ContentTypeField(
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
'custom_fields', 'created', 'last_updated', '_depth',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_component(self, obj):
if obj.component is None:
return None
serializer = get_serializer_for_model(obj.component, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.component, context=context).data
#
# Device component roles
#
class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
class Meta:
model = InventoryItemRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'inventoryitem_count',
] ]
@ -743,7 +895,7 @@ class InventoryItemSerializer(PrimaryModelSerializer):
# Cables # Cables
# #
class CableSerializer(PrimaryModelSerializer): class CableSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField( termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@ -849,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisSerializer(PrimaryModelSerializer): class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False) master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
@ -866,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
# Power panels # Power panels
# #
class PowerPanelSerializer(PrimaryModelSerializer): class PowerPanelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
location = NestedLocationSerializer( location = NestedLocationSerializer(
@ -884,7 +1036,7 @@ class PowerPanelSerializer(PrimaryModelSerializer):
] ]
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer() power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer( rack = NestedRackSerializer(

View File

@ -1,8 +1,8 @@
from netbox.api import OrderedDefaultRouter from netbox.api import NetBoxRouter
from . import views from . import views
router = OrderedDefaultRouter() router = NetBoxRouter()
router.APIRootView = views.DCIMRootView router.APIRootView = views.DCIMRootView
# Sites # Sites
@ -16,9 +16,10 @@ router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)
# Device types # Device/module types
router.register('manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register('device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
router.register('module-types', views.ModuleTypeViewSet)
# Device type components # Device type components
router.register('console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)
@ -28,12 +29,15 @@ router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register('interface-templates', views.InterfaceTemplateViewSet) router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register('front-port-templates', views.FrontPortTemplateViewSet) router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register('rear-port-templates', views.RearPortTemplateViewSet) router.register('rear-port-templates', views.RearPortTemplateViewSet)
router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
router.register('device-bay-templates', views.DeviceBayTemplateViewSet) router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
router.register('inventory-item-templates', views.InventoryItemTemplateViewSet)
# Devices # Device/modules
router.register('device-roles', views.DeviceRoleViewSet) router.register('device-roles', views.DeviceRoleViewSet)
router.register('platforms', views.PlatformViewSet) router.register('platforms', views.PlatformViewSet)
router.register('devices', views.DeviceViewSet) router.register('devices', views.DeviceViewSet)
router.register('modules', views.ModuleViewSet)
# Device components # Device components
router.register('console-ports', views.ConsolePortViewSet) router.register('console-ports', views.ConsolePortViewSet)
@ -43,9 +47,13 @@ router.register('power-outlets', views.PowerOutletViewSet)
router.register('interfaces', views.InterfaceViewSet) router.register('interfaces', views.InterfaceViewSet)
router.register('front-ports', views.FrontPortViewSet) router.register('front-ports', views.FrontPortViewSet)
router.register('rear-ports', views.RearPortViewSet) router.register('rear-ports', views.RearPortViewSet)
router.register('module-bays', views.ModuleBayViewSet)
router.register('device-bays', views.DeviceBayViewSet) router.register('device-bays', views.DeviceBayViewSet)
router.register('inventory-items', views.InventoryItemViewSet) router.register('inventory-items', views.InventoryItemViewSet)
# Device component roles
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
# Cables # Cables
router.register('cables', views.CableViewSet) router.register('cables', views.CableViewSet)

View File

@ -14,12 +14,12 @@ from rest_framework.viewsets import ViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim import filtersets from dcim import filtersets
from dcim.models import * from dcim.models import *
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet from extras.api.views import ConfigContextQuerySetMixin
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata from netbox.api.metadata import ContentTypeMetadata
from netbox.api.views import ModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from netbox.config import get_config from netbox.config import get_config
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import count_related from utilities.utils import count_related
@ -103,7 +103,7 @@ class PassThroughPortMixin(object):
# Regions # Regions
# #
class RegionViewSet(CustomFieldModelViewSet): class RegionViewSet(NetBoxModelViewSet):
queryset = Region.objects.add_related_count( queryset = Region.objects.add_related_count(
Region.objects.all(), Region.objects.all(),
Site, Site,
@ -119,7 +119,7 @@ class RegionViewSet(CustomFieldModelViewSet):
# Site groups # Site groups
# #
class SiteGroupViewSet(CustomFieldModelViewSet): class SiteGroupViewSet(NetBoxModelViewSet):
queryset = SiteGroup.objects.add_related_count( queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(), SiteGroup.objects.all(),
Site, Site,
@ -135,7 +135,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
# Sites # Sites
# #
class SiteViewSet(CustomFieldModelViewSet): class SiteViewSet(NetBoxModelViewSet):
queryset = Site.objects.prefetch_related( queryset = Site.objects.prefetch_related(
'region', 'tenant', 'asns', 'tags' 'region', 'tenant', 'asns', 'tags'
).annotate( ).annotate(
@ -154,7 +154,7 @@ class SiteViewSet(CustomFieldModelViewSet):
# Locations # Locations
# #
class LocationViewSet(CustomFieldModelViewSet): class LocationViewSet(NetBoxModelViewSet):
queryset = Location.objects.add_related_count( queryset = Location.objects.add_related_count(
Location.objects.add_related_count( Location.objects.add_related_count(
Location.objects.all(), Location.objects.all(),
@ -176,7 +176,7 @@ class LocationViewSet(CustomFieldModelViewSet):
# Rack roles # Rack roles
# #
class RackRoleViewSet(CustomFieldModelViewSet): class RackRoleViewSet(NetBoxModelViewSet):
queryset = RackRole.objects.prefetch_related('tags').annotate( queryset = RackRole.objects.prefetch_related('tags').annotate(
rack_count=count_related(Rack, 'role') rack_count=count_related(Rack, 'role')
) )
@ -188,7 +188,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
# Racks # Racks
# #
class RackViewSet(CustomFieldModelViewSet): class RackViewSet(NetBoxModelViewSet):
queryset = Rack.objects.prefetch_related( queryset = Rack.objects.prefetch_related(
'site', 'location', 'role', 'tenant', 'tags' 'site', 'location', 'role', 'tenant', 'tags'
).annotate( ).annotate(
@ -250,7 +250,7 @@ class RackViewSet(CustomFieldModelViewSet):
# Rack reservations # Rack reservations
# #
class RackReservationViewSet(ModelViewSet): class RackReservationViewSet(NetBoxModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer serializer_class = serializers.RackReservationSerializer
filterset_class = filtersets.RackReservationFilterSet filterset_class = filtersets.RackReservationFilterSet
@ -260,7 +260,7 @@ class RackReservationViewSet(ModelViewSet):
# Manufacturers # Manufacturers
# #
class ManufacturerViewSet(CustomFieldModelViewSet): class ManufacturerViewSet(NetBoxModelViewSet):
queryset = Manufacturer.objects.prefetch_related('tags').annotate( queryset = Manufacturer.objects.prefetch_related('tags').annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'), devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
@ -271,10 +271,10 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
# #
# Device types # Device/module types
# #
class DeviceTypeViewSet(CustomFieldModelViewSet): class DeviceTypeViewSet(NetBoxModelViewSet):
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate( queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
device_count=count_related(Device, 'device_type') device_count=count_related(Device, 'device_type')
) )
@ -283,63 +283,84 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
brief_prefetch_fields = ['manufacturer'] brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(NetBoxModelViewSet):
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
# module_count=count_related(Module, 'module_type')
)
serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet
brief_prefetch_fields = ['manufacturer']
# #
# Device type components # Device type components
# #
class ConsolePortTemplateViewSet(ModelViewSet): class ConsolePortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filtersets.ConsolePortTemplateFilterSet filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(ModelViewSet): class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(ModelViewSet): class PowerPortTemplateViewSet(NetBoxModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filtersets.PowerPortTemplateFilterSet filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(ModelViewSet): class PowerOutletTemplateViewSet(NetBoxModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filtersets.PowerOutletTemplateFilterSet filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(ModelViewSet): class InterfaceTemplateViewSet(NetBoxModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filtersets.InterfaceTemplateFilterSet filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(ModelViewSet): class FrontPortTemplateViewSet(NetBoxModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filtersets.FrontPortTemplateFilterSet filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(ModelViewSet): class RearPortTemplateViewSet(NetBoxModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filtersets.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet): class ModuleBayTemplateViewSet(NetBoxModelViewSet):
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
# #
# Device roles # Device roles
# #
class DeviceRoleViewSet(CustomFieldModelViewSet): class DeviceRoleViewSet(NetBoxModelViewSet):
queryset = DeviceRole.objects.prefetch_related('tags').annotate( queryset = DeviceRole.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'device_role'), device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role') virtualmachine_count=count_related(VirtualMachine, 'role')
@ -352,7 +373,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
# Platforms # Platforms
# #
class PlatformViewSet(CustomFieldModelViewSet): class PlatformViewSet(NetBoxModelViewSet):
queryset = Platform.objects.prefetch_related('tags').annotate( queryset = Platform.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'platform'), device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform') virtualmachine_count=count_related(VirtualMachine, 'platform')
@ -362,10 +383,10 @@ class PlatformViewSet(CustomFieldModelViewSet):
# #
# Devices # Devices/modules
# #
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
queryset = Device.objects.prefetch_related( queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay', 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
@ -511,83 +532,120 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
return Response(response) return Response(response)
class ModuleViewSet(NetBoxModelViewSet):
queryset = Module.objects.prefetch_related(
'device', 'module_bay', 'module_type__manufacturer', 'tags',
)
serializer_class = serializers.ModuleSerializer
filterset_class = filtersets.ModuleFilterSet
# #
# Device components # Device components
# #
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = ConsolePort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related( queryset = ConsoleServerPort.objects.prefetch_related(
'device', '_path__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
) )
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = PowerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = PowerOutlet.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer', 'wireless_lans', 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags' 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
)
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, ModelViewSet): class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
)
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet): class ModuleBayViewSet(NetBoxModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') queryset = ModuleBay.objects.prefetch_related('tags')
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']
class DeviceBayViewSet(NetBoxModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet): class InventoryItemViewSet(NetBoxModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
#
# Device component roles
#
class InventoryItemRoleViewSet(NetBoxModelViewSet):
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
inventoryitem_count=count_related(InventoryItem, 'role')
)
serializer_class = serializers.InventoryItemRoleSerializer
filterset_class = filtersets.InventoryItemRoleFilterSet
# #
# Cables # Cables
# #
class CableViewSet(ModelViewSet): class CableViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = Cable.objects.prefetch_related( queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b' 'termination_a', 'termination_b'
@ -600,7 +658,7 @@ class CableViewSet(ModelViewSet):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisViewSet(ModelViewSet): class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags').annotate( queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
member_count=count_related(Device, 'virtual_chassis') member_count=count_related(Device, 'virtual_chassis')
) )
@ -613,7 +671,7 @@ class VirtualChassisViewSet(ModelViewSet):
# Power panels # Power panels
# #
class PowerPanelViewSet(ModelViewSet): class PowerPanelViewSet(NetBoxModelViewSet):
queryset = PowerPanel.objects.prefetch_related( queryset = PowerPanel.objects.prefetch_related(
'site', 'location' 'site', 'location'
).annotate( ).annotate(
@ -627,7 +685,7 @@ class PowerPanelViewSet(ModelViewSet):
# Power feeds # Power feeds
# #
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet): class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related( queryset = PowerFeed.objects.prefetch_related(
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags' 'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
) )

View File

@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
# #
class SiteStatusChoices(ChoiceSet): class SiteStatusChoices(ChoiceSet):
key = 'Site.status'
STATUS_PLANNED = 'planned' STATUS_PLANNED = 'planned'
STATUS_STAGING = 'staging' STATUS_STAGING = 'staging'
@ -13,21 +14,13 @@ class SiteStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
STATUS_RETIRED = 'retired' STATUS_RETIRED = 'retired'
CHOICES = ( CHOICES = [
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGING, 'Staging'), (STATUS_STAGING, 'Staging', 'blue'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'green'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_RETIRED, 'Retired'), (STATUS_RETIRED, 'Retired', 'red'),
) ]
CSS_CLASSES = {
STATUS_PLANNED: 'info',
STATUS_STAGING: 'primary',
STATUS_ACTIVE: 'success',
STATUS_DECOMMISSIONING: 'warning',
STATUS_RETIRED: 'danger',
}
# #
@ -67,6 +60,7 @@ class RackWidthChoices(ChoiceSet):
class RackStatusChoices(ChoiceSet): class RackStatusChoices(ChoiceSet):
key = 'Rack.status'
STATUS_RESERVED = 'reserved' STATUS_RESERVED = 'reserved'
STATUS_AVAILABLE = 'available' STATUS_AVAILABLE = 'available'
@ -74,21 +68,13 @@ class RackStatusChoices(ChoiceSet):
STATUS_ACTIVE = 'active' STATUS_ACTIVE = 'active'
STATUS_DEPRECATED = 'deprecated' STATUS_DEPRECATED = 'deprecated'
CHOICES = ( CHOICES = [
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'yellow'),
(STATUS_AVAILABLE, 'Available'), (STATUS_AVAILABLE, 'Available', 'green'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'blue'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'red'),
) ]
CSS_CLASSES = {
STATUS_RESERVED: 'warning',
STATUS_AVAILABLE: 'success',
STATUS_PLANNED: 'info',
STATUS_ACTIVE: 'primary',
STATUS_DEPRECATED: 'danger',
}
class RackDimensionUnitChoices(ChoiceSet): class RackDimensionUnitChoices(ChoiceSet):
@ -144,6 +130,7 @@ class DeviceFaceChoices(ChoiceSet):
class DeviceStatusChoices(ChoiceSet): class DeviceStatusChoices(ChoiceSet):
key = 'Device.status'
STATUS_OFFLINE = 'offline' STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active' STATUS_ACTIVE = 'active'
@ -153,25 +140,15 @@ class DeviceStatusChoices(ChoiceSet):
STATUS_INVENTORY = 'inventory' STATUS_INVENTORY = 'inventory'
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = ( CHOICES = [
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGED, 'Staged'), (STATUS_STAGED, 'Staged', 'blue'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed', 'red'),
(STATUS_INVENTORY, 'Inventory'), (STATUS_INVENTORY, 'Inventory', 'purple'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
) ]
CSS_CLASSES = {
STATUS_OFFLINE: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_STAGED: 'primary',
STATUS_FAILED: 'danger',
STATUS_INVENTORY: 'secondary',
STATUS_DECOMMISSIONING: 'warning',
}
class DeviceAirflowChoices(ChoiceSet): class DeviceAirflowChoices(ChoiceSet):
@ -974,6 +951,19 @@ class InterfaceTypeChoices(ChoiceSet):
) )
class InterfaceDuplexChoices(ChoiceSet):
DUPLEX_HALF = 'half'
DUPLEX_FULL = 'full'
DUPLEX_AUTO = 'auto'
CHOICES = (
(DUPLEX_HALF, 'Half'),
(DUPLEX_FULL, 'Full'),
(DUPLEX_AUTO, 'Auto'),
)
class InterfaceModeChoices(ChoiceSet): class InterfaceModeChoices(ChoiceSet):
MODE_ACCESS = 'access' MODE_ACCESS = 'access'
@ -1164,17 +1154,11 @@ class LinkStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = ( CHOICES = (
(STATUS_CONNECTED, 'Connected'), (STATUS_CONNECTED, 'Connected', 'green'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'blue'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
) )
CSS_CLASSES = {
STATUS_CONNECTED: 'success',
STATUS_PLANNED: 'info',
STATUS_DECOMMISSIONING: 'warning',
}
class CableLengthUnitChoices(ChoiceSet): class CableLengthUnitChoices(ChoiceSet):
@ -1203,25 +1187,19 @@ class CableLengthUnitChoices(ChoiceSet):
# #
class PowerFeedStatusChoices(ChoiceSet): class PowerFeedStatusChoices(ChoiceSet):
key = 'PowerFeed.status'
STATUS_OFFLINE = 'offline' STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active' STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned' STATUS_PLANNED = 'planned'
STATUS_FAILED = 'failed' STATUS_FAILED = 'failed'
CHOICES = ( CHOICES = [
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'blue'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed', 'red'),
) ]
CSS_CLASSES = {
STATUS_OFFLINE: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_FAILED: 'danger',
}
class PowerFeedTypeChoices(ChoiceSet): class PowerFeedTypeChoices(ChoiceSet):
@ -1230,15 +1208,10 @@ class PowerFeedTypeChoices(ChoiceSet):
TYPE_REDUNDANT = 'redundant' TYPE_REDUNDANT = 'redundant'
CHOICES = ( CHOICES = (
(TYPE_PRIMARY, 'Primary'), (TYPE_PRIMARY, 'Primary', 'green'),
(TYPE_REDUNDANT, 'Redundant'), (TYPE_REDUNDANT, 'Redundant', 'cyan'),
) )
CSS_CLASSES = {
TYPE_PRIMARY: 'success',
TYPE_REDUNDANT: 'info',
}
class PowerFeedSupplyChoices(ChoiceSet): class PowerFeedSupplyChoices(ChoiceSet):

View File

@ -50,16 +50,43 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
# #
# PowerFeeds # Power feeds
# #
POWERFEED_VOLTAGE_DEFAULT = 120 POWERFEED_VOLTAGE_DEFAULT = 120
POWERFEED_AMPERAGE_DEFAULT = 20 POWERFEED_AMPERAGE_DEFAULT = 20
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
#
# Device components
#
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
app_label='dcim',
model__in=(
'consoleporttemplate',
'consoleserverporttemplate',
'frontporttemplate',
'interfacetemplate',
'poweroutlettemplate',
'powerporttemplate',
'rearporttemplate',
))
MODULAR_COMPONENT_MODELS = Q(
app_label='dcim',
model__in=(
'consoleport',
'consoleserverport',
'frontport',
'interface',
'poweroutlet',
'powerport',
'rearport',
))
# #
# Cabling and connections # Cabling and connections
# #

View File

@ -1,11 +1,10 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from extras.filters import TagFilter
from extras.filtersets import LocalConfigContextFilterSet from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import ASN from ipam.models import ASN, VRF
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet, BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
) )
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import * from tenancy.models import *
@ -39,8 +38,14 @@ __all__ = (
'InterfaceFilterSet', 'InterfaceFilterSet',
'InterfaceTemplateFilterSet', 'InterfaceTemplateFilterSet',
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'InventoryItemRoleFilterSet',
'InventoryItemTemplateFilterSet',
'LocationFilterSet', 'LocationFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
'ModuleFilterSet',
'ModuleTypeFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
'PowerConnectionFilterSet', 'PowerConnectionFilterSet',
@ -73,7 +78,6 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Parent region (slug)', label='Parent region (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = Region model = Region
@ -91,18 +95,13 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Parent site group (slug)', label='Parent site group (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=SiteStatusChoices, choices=SiteStatusChoices,
null_value=None null_value=None
@ -131,19 +130,23 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
to_field_name='slug', to_field_name='slug',
label='Group (slug)', label='Group (slug)',
) )
asn = django_filters.ModelMultipleChoiceFilter(
field_name='asns__asn',
queryset=ASN.objects.all(),
to_field_name='asn',
label='AS (ID)',
)
asn_id = django_filters.ModelMultipleChoiceFilter( asn_id = django_filters.ModelMultipleChoiceFilter(
field_name='asns', field_name='asns',
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
label='AS (ID)', label='AS (ID)',
) )
tag = TagFilter()
class Meta: class Meta:
model = Site model = Site
fields = [ fields = (
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description'
'contact_email', 'description' )
]
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -154,13 +157,9 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
Q(description__icontains=value) | Q(description__icontains=value) |
Q(physical_address__icontains=value) | Q(physical_address__icontains=value) |
Q(shipping_address__icontains=value) | Q(shipping_address__icontains=value) |
Q(contact_name__icontains=value) |
Q(contact_phone__icontains=value) |
Q(contact_email__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
try: try:
qs_filter |= Q(asn=int(value.strip()))
qs_filter |= Q(asns__asn=int(value.strip())) qs_filter |= Q(asns__asn=int(value.strip()))
except ValueError: except ValueError:
pass pass
@ -217,7 +216,6 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
to_field_name='slug', to_field_name='slug',
label='Location (slug)', label='Location (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = Location model = Location
@ -233,18 +231,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
class RackRoleFilterSet(OrganizationalModelFilterSet): class RackRoleFilterSet(OrganizationalModelFilterSet):
tag = TagFilter()
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -317,7 +310,6 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
serial = django_filters.CharFilter( serial = django_filters.CharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )
tag = TagFilter()
class Meta: class Meta:
model = Rack model = Rack
@ -338,11 +330,7 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
) )
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
@ -381,7 +369,6 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
to_field_name='username', to_field_name='username',
label='User (name)', label='User (name)',
) )
tag = TagFilter()
class Meta: class Meta:
model = RackReservation model = RackReservation
@ -399,18 +386,13 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter()
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class DeviceTypeFilterSet(PrimaryModelFilterSet): class DeviceTypeFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)', label='Manufacturer (ID)',
@ -445,11 +427,14 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter( device_bays = django_filters.BooleanFilter(
method='_device_bays', method='_device_bays',
label='Has device bays', label='Has device bays',
) )
tag = TagFilter()
class Meta: class Meta:
model = DeviceType model = DeviceType
@ -488,10 +473,85 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
rearporttemplates__isnull=value rearporttemplates__isnull=value
) )
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebaytemplates__isnull=value)
def _device_bays(self, queryset, name, value): def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebaytemplates__isnull=value) return queryset.exclude(devicebaytemplates__isnull=value)
class ModuleTypeFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label='Has console server ports',
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label='Has power ports',
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label='Has power outlets',
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label='Has interfaces',
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label='Has pass-through ports',
)
class Meta:
model = ModuleType
fields = ['id', 'model', 'part_number']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(manufacturer__name__icontains=value) |
Q(model__icontains=value) |
Q(part_number__icontains=value) |
Q(comments__icontains=value)
)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleporttemplates__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverporttemplates__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerporttemplates__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlettemplates__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfacetemplates__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontporttemplates__isnull=value,
rearporttemplates__isnull=value
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
@ -509,28 +569,36 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
return queryset.filter(name__icontains=value) return queryset.filter(name__icontains=value)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
moduletype_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleType.objects.all(),
field_name='module_type_id',
label='Module type (ID)',
)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter( feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
@ -541,7 +609,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompone
fields = ['id', 'name', 'type', 'feed_leg'] fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
null_value=None null_value=None
@ -552,7 +620,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'mgmt_only'] fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -563,7 +631,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'color'] fields = ['id', 'name', 'type', 'color']
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -574,6 +642,13 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
fields = ['id', 'name', 'type', 'color', 'positions'] fields = ['id', 'name', 'type', 'color', 'positions']
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
model = ModuleBayTemplate
fields = ['id', 'name']
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
@ -581,8 +656,50 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name'] fields = ['id', 'name']
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemTemplate.objects.all(),
label='Parent inventory item (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
field_name='role__slug',
queryset=InventoryItemRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
component_type = ContentTypeFilter()
component_id = MultiValueNumberFilter()
class Meta:
model = InventoryItemTemplate
fields = ['id', 'name', 'label', 'part_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(part_id__icontains=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)
class DeviceRoleFilterSet(OrganizationalModelFilterSet): class DeviceRoleFilterSet(OrganizationalModelFilterSet):
tag = TagFilter()
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@ -601,18 +718,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Manufacturer (slug)', label='Manufacturer (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet): class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__manufacturer', field_name='device_type__manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -763,11 +875,14 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter( device_bays = django_filters.BooleanFilter(
method='_device_bays', method='_device_bays',
label='Has device bays', label='Has device bays',
) )
tag = TagFilter()
class Meta: class Meta:
model = Device model = Device
@ -814,10 +929,55 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
rearports__isnull=value rearports__isnull=value
) )
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebays__isnull=value)
def _device_bays(self, queryset, name, value): def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebays__isnull=value) return queryset.exclude(devicebays__isnull=value)
class ModuleFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
module_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type',
queryset=ModuleType.objects.all(),
label='Module type (ID)',
)
module_type = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__model',
queryset=ModuleType.objects.all(),
to_field_name='model',
label='Module type (model)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
class Meta:
model = Module
fields = ['id', 'serial', 'asset_tag']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
).distinct()
class DeviceComponentFilterSet(django_filters.FilterSet): class DeviceComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
@ -892,7 +1052,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='name', to_field_name='name',
label='Virtual Chassis', label='Virtual Chassis',
) )
tag = TagFilter()
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -904,6 +1063,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
) )
class ModularDeviceComponentFilterSet(DeviceComponentFilterSet):
"""
Extends DeviceComponentFilterSet to add a module_id filter for components
which can be associated with a particular module within a device.
"""
module_id = django_filters.ModelMultipleChoiceFilter(
queryset=Module.objects.all(),
label='Module (ID)',
)
class CableTerminationFilterSet(django_filters.FilterSet): class CableTerminationFilterSet(django_filters.FilterSet):
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
@ -924,7 +1094,12 @@ class PathEndpointFilterSet(django_filters.FilterSet):
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class ConsolePortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@ -935,7 +1110,12 @@ class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class ConsoleServerPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@ -946,7 +1126,12 @@ class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
null_value=None null_value=None
@ -957,7 +1142,12 @@ class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerOutletFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
null_value=None null_value=None
@ -972,11 +1162,12 @@ class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
fields = ['id', 'name', 'label', 'feed_leg', 'description'] fields = ['id', 'name', 'label', 'feed_leg', 'description']
class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class InterfaceFilterSet(
q = django_filters.CharFilter( NetBoxModelFilterSet,
method='search', ModularDeviceComponentFilterSet,
label='Search', CableTerminationFilterSet,
) PathEndpointFilterSet
):
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis # Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
# members # members
device = MultiValueCharFilter( device = MultiValueCharFilter(
@ -1008,9 +1199,12 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='LAG interface (ID)', label='LAG interface (ID)',
) )
speed = MultiValueNumberFilter()
duplex = django_filters.MultipleChoiceFilter(
choices=InterfaceDuplexChoices
)
mac_address = MultiValueMACAddressFilter() mac_address = MultiValueMACAddressFilter()
wwn = MultiValueWWNFilter() wwn = MultiValueWWNFilter()
tag = TagFilter()
vlan_id = django_filters.CharFilter( vlan_id = django_filters.CharFilter(
method='filter_vlan_id', method='filter_vlan_id',
label='Assigned VLAN' label='Assigned VLAN'
@ -1029,6 +1223,17 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
rf_channel = django_filters.MultipleChoiceFilter( rf_channel = django_filters.MultipleChoiceFilter(
choices=WirelessChannelChoices choices=WirelessChannelChoices
) )
vrf_id = django_filters.ModelMultipleChoiceFilter(
field_name='vrf',
queryset=VRF.objects.all(),
label='VRF',
)
vrf = django_filters.ModelMultipleChoiceFilter(
field_name='vrf__rd',
queryset=VRF.objects.all(),
to_field_name='rd',
label='VRF (RD)',
)
class Meta: class Meta:
model = Interface model = Interface
@ -1085,7 +1290,11 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
}.get(value, queryset.none()) }.get(value, queryset.none())
class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class FrontPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -1096,7 +1305,11 @@ class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
fields = ['id', 'name', 'label', 'type', 'color', 'description'] fields = ['id', 'name', 'label', 'type', 'color', 'description']
class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class RearPortFilterSet(
NetBoxModelFilterSet,
ModularDeviceComponentFilterSet,
CableTerminationFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -1107,18 +1320,21 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description'] fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): class ModuleBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
class Meta:
model = ModuleBay
fields = ['id', 'name', 'label', 'description']
class DeviceBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description']
class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)', label='Parent inventory item (ID)',
@ -1133,6 +1349,18 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
to_field_name='slug', to_field_name='slug',
label='Manufacturer (slug)', label='Manufacturer (slug)',
) )
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
field_name='role__slug',
queryset=InventoryItemRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
component_type = ContentTypeFilter()
component_id = MultiValueNumberFilter()
serial = django_filters.CharFilter( serial = django_filters.CharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )
@ -1154,11 +1382,14 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class VirtualChassisFilterSet(PrimaryModelFilterSet): class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
q = django_filters.CharFilter(
method='search', class Meta:
label='Search', model = InventoryItemRole
) fields = ['id', 'name', 'slug', 'color']
class VirtualChassisFilterSet(NetBoxModelFilterSet):
master_id = django_filters.ModelMultipleChoiceFilter( master_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Master (ID)', label='Master (ID)',
@ -1217,7 +1448,6 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
tag = TagFilter()
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
@ -1234,11 +1464,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
return queryset.filter(qs_filter).distinct() return queryset.filter(qs_filter).distinct()
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
termination_a_type = ContentTypeFilter() termination_a_type = ContentTypeFilter()
termination_a_id = MultiValueNumberFilter() termination_a_id = MultiValueNumberFilter()
termination_b_type = ContentTypeFilter() termination_b_type = ContentTypeFilter()
@ -1275,7 +1501,6 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
method='filter_device', method='filter_device',
field_name='device__site__slug' field_name='device__site__slug'
) )
tag = TagFilter()
class Meta: class Meta:
model = Cable model = Cable
@ -1294,11 +1519,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
return queryset return queryset
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -1341,7 +1562,6 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
lookup_expr='in', lookup_expr='in',
label='Location (ID)', label='Location (ID)',
) )
tag = TagFilter()
class Meta: class Meta:
model = PowerPanel model = PowerPanel
@ -1356,11 +1576,7 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='power_panel__site__region', field_name='power_panel__site__region',
@ -1411,7 +1627,6 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
null_value=None null_value=None
) )
tag = TagFilter()
class Meta: class Meta:
model = PowerFeed model = PowerFeed

View File

@ -4,7 +4,7 @@ from dcim.models import *
from extras.forms import CustomFieldsMixin from extras.forms import CustomFieldsMixin
from extras.models import Tag from extras.models import Tag
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
from .object_create import ComponentForm from .object_create import ComponentCreateForm
__all__ = ( __all__ = (
'ConsolePortBulkCreateForm', 'ConsolePortBulkCreateForm',
@ -13,6 +13,7 @@ __all__ = (
# 'FrontPortBulkCreateForm', # 'FrontPortBulkCreateForm',
'InterfaceBulkCreateForm', 'InterfaceBulkCreateForm',
'InventoryItemBulkCreateForm', 'InventoryItemBulkCreateForm',
'ModuleBayBulkCreateForm',
'PowerOutletBulkCreateForm', 'PowerOutletBulkCreateForm',
'PowerPortBulkCreateForm', 'PowerPortBulkCreateForm',
'RearPortBulkCreateForm', 'RearPortBulkCreateForm',
@ -23,7 +24,7 @@ __all__ = (
# Device components # Device components
# #
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm): class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
@ -71,12 +72,12 @@ class PowerOutletBulkCreateForm(
class InterfaceBulkCreateForm( class InterfaceBulkCreateForm(
form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected']), form_from_model(Interface, ['type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected']),
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = Interface model = Interface
field_order = ( field_order = (
'name_pattern', 'label_pattern', 'type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags', 'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
) )
@ -95,17 +96,22 @@ class RearPortBulkCreateForm(
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags') field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm): class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
model = DeviceBay model = DeviceBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags') field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
class InventoryItemBulkCreateForm( class InventoryItemBulkCreateForm(
form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']), form_from_model(InventoryItem, ['role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = InventoryItem model = InventoryItem
field_order = ( field_order = (
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'tags', 'description', 'tags',
) )

View File

@ -6,13 +6,12 @@ from timezone_field import TimeZoneFormField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import ASN, VLAN, VRF
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX from netbox.forms import NetBoxModelBulkEditForm
from ipam.models import VLAN, ASN
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
) )
__all__ = ( __all__ = (
@ -31,8 +30,14 @@ __all__ = (
'InterfaceBulkEditForm', 'InterfaceBulkEditForm',
'InterfaceTemplateBulkEditForm', 'InterfaceTemplateBulkEditForm',
'InventoryItemBulkEditForm', 'InventoryItemBulkEditForm',
'InventoryItemRoleBulkEditForm',
'InventoryItemTemplateBulkEditForm',
'LocationBulkEditForm', 'LocationBulkEditForm',
'ManufacturerBulkEditForm', 'ManufacturerBulkEditForm',
'ModuleBulkEditForm',
'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm',
'ModuleTypeBulkEditForm',
'PlatformBulkEditForm', 'PlatformBulkEditForm',
'PowerFeedBulkEditForm', 'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm', 'PowerOutletBulkEditForm',
@ -52,11 +57,7 @@ __all__ = (
) )
class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): class RegionBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Region.objects.all(),
widget=forms.MultipleHiddenInput
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
@ -66,15 +67,14 @@ class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = Region
nullable_fields = ['parent', 'description'] fieldsets = (
(None, ('parent', 'description')),
class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('parent', 'description')
class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
@ -84,15 +84,14 @@ class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = SiteGroup
nullable_fields = ['parent', 'description'] fieldsets = (
(None, ('parent', 'description')),
class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Site.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('parent', 'description')
class SiteBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(SiteStatusChoices), choices=add_blank_choice(SiteStatusChoices),
required=False, required=False,
@ -111,12 +110,6 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
asn = forms.IntegerField(
min_value=BGP_ASN_MIN,
max_value=BGP_ASN_MAX,
required=False,
label='ASN'
)
asns = DynamicModelMultipleChoiceField( asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
label=_('ASNs'), label=_('ASNs'),
@ -144,18 +137,16 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
widget=StaticSelect() widget=StaticSelect()
) )
class Meta: model = Site
nullable_fields = [ fieldsets = (
'region', 'group', 'tenant', 'asn', 'asns', 'contact_name', 'contact_phone', 'contact_email', 'description', (None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
'time_zone',
]
class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Location.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = (
'region', 'group', 'tenant', 'asns', 'description', 'time_zone',
)
class LocationBulkEditForm(NetBoxModelBulkEditForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
@ -176,15 +167,14 @@ class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = Location
nullable_fields = ['parent', 'tenant', 'description'] fieldsets = (
(None, ('site', 'parent', 'tenant', 'description')),
class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RackRole.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('parent', 'tenant', 'description')
class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField( color = ColorField(
required=False required=False
) )
@ -193,15 +183,14 @@ class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = RackRole
nullable_fields = ['color', 'description'] fieldsets = (
(None, ('color', 'description')),
class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Rack.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('color', 'description')
class RackBulkEditForm(NetBoxModelBulkEditForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -291,17 +280,18 @@ class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label='Comments' label='Comments'
) )
class Meta: model = Rack
nullable_fields = [ fieldsets = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
] ('Location', ('region', 'site_group', 'site', 'location')),
('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RackReservation.objects.all(),
widget=forms.MultipleHiddenInput()
) )
nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
)
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
user = forms.ModelChoiceField( user = forms.ModelChoiceField(
queryset=User.objects.order_by( queryset=User.objects.order_by(
'username' 'username'
@ -318,33 +308,33 @@ class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor
required=False required=False
) )
class Meta: model = RackReservation
nullable_fields = [] fieldsets = (
(None, ('user', 'tenant', 'description')),
class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
widget=forms.MultipleHiddenInput
) )
class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
) )
class Meta: model = Manufacturer
nullable_fields = ['description'] fieldsets = (
(None, ('description',)),
class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
widget=forms.MultipleHiddenInput()
) )
nullable_fields = ('description',)
class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
part_number = forms.CharField(
required=False
)
u_height = forms.IntegerField( u_height = forms.IntegerField(
min_value=1, min_value=1,
required=False required=False
@ -360,15 +350,30 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
widget=StaticSelect() widget=StaticSelect()
) )
class Meta: model = DeviceType
nullable_fields = ['airflow'] fieldsets = (
(None, ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('part_number', 'airflow')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
part_number = forms.CharField(
required=False
)
model = ModuleType
fieldsets = (
(None, ('manufacturer', 'part_number')),
)
nullable_fields = ('part_number',)
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField( color = ColorField(
required=False required=False
) )
@ -382,15 +387,14 @@ class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = DeviceRole
nullable_fields = ['color', 'description'] fieldsets = (
(None, ('color', 'vm_role', 'description')),
class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Platform.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('color', 'description')
class PlatformBulkEditForm(NetBoxModelBulkEditForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
@ -405,15 +409,14 @@ class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
required=False required=False
) )
class Meta: model = Platform
nullable_fields = ['manufacturer', 'napalm_driver', 'description'] fieldsets = (
(None, ('manufacturer', 'napalm_driver', 'description')),
class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput()
) )
nullable_fields = ('manufacturer', 'napalm_driver', 'description')
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
@ -464,17 +467,43 @@ class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label='Serial Number' label='Serial Number'
) )
class Meta: model = Device
nullable_fields = [ fieldsets = (
'tenant', 'platform', 'serial', 'airflow', ('Device', ('device_role', 'status', 'tenant', 'platform')),
] ('Location', ('site', 'location')),
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Cable.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = (
'tenant', 'platform', 'serial', 'airflow',
)
class ModuleBulkEditForm(NetBoxModelBulkEditForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer'
}
)
serial = forms.CharField(
max_length=50,
required=False,
label='Serial Number'
)
model = Module
fieldsets = (
(None, ('manufacturer', 'module_type', 'serial')),
)
nullable_fields = ('serial',)
class CableBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(CableTypeChoices), choices=add_blank_choice(CableTypeChoices),
required=False, required=False,
@ -509,10 +538,14 @@ class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
widget=StaticSelect() widget=StaticSelect()
) )
class Meta: model = Cable
nullable_fields = [ fieldsets = (
'type', 'status', 'tenant', 'label', 'color', 'length', (None, ('type', 'status', 'tenant', 'label')),
] ('Attributes', ('color', 'length', 'length_unit')),
)
nullable_fields = (
'type', 'status', 'tenant', 'label', 'color', 'length',
)
def clean(self): def clean(self):
super().clean() super().clean()
@ -526,25 +559,20 @@ class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
}) })
class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=VirtualChassis.objects.all(),
widget=forms.MultipleHiddenInput()
)
domain = forms.CharField( domain = forms.CharField(
max_length=30, max_length=30,
required=False required=False
) )
class Meta: model = VirtualChassis
nullable_fields = ['domain'] fieldsets = (
(None, ('domain',)),
class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('domain',)
class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -575,15 +603,14 @@ class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
} }
) )
class Meta: model = PowerPanel
nullable_fields = ['location'] fieldsets = (
(None, ('region', 'site_group', 'site', 'location')),
class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=PowerFeed.objects.all(),
widget=forms.MultipleHiddenInput
) )
nullable_fields = ('location',)
class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
power_panel = DynamicModelChoiceField( power_panel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all(), queryset=PowerPanel.objects.all(),
required=False required=False
@ -634,10 +661,12 @@ class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label='Comments' label='Comments'
) )
class Meta: model = PowerFeed
nullable_fields = [ fieldsets = (
'location', 'comments', (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected')),
] ('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
)
nullable_fields = ('location', 'comments')
# #
@ -659,8 +688,7 @@ class ConsolePortTemplateBulkEditForm(BulkEditForm):
widget=StaticSelect() widget=StaticSelect()
) )
class Meta: nullable_fields = ('label', 'type', 'description')
nullable_fields = ('label', 'type', 'description')
class ConsoleServerPortTemplateBulkEditForm(BulkEditForm): class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
@ -681,8 +709,7 @@ class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('label', 'type', 'description')
nullable_fields = ('label', 'type', 'description')
class PowerPortTemplateBulkEditForm(BulkEditForm): class PowerPortTemplateBulkEditForm(BulkEditForm):
@ -713,8 +740,7 @@ class PowerPortTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
class PowerOutletTemplateBulkEditForm(BulkEditForm): class PowerOutletTemplateBulkEditForm(BulkEditForm):
@ -750,8 +776,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description')
nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -788,8 +813,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('label', 'description')
nullable_fields = ('label', 'description')
class FrontPortTemplateBulkEditForm(BulkEditForm): class FrontPortTemplateBulkEditForm(BulkEditForm):
@ -813,8 +837,7 @@ class FrontPortTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('description',)
nullable_fields = ('description',)
class RearPortTemplateBulkEditForm(BulkEditForm): class RearPortTemplateBulkEditForm(BulkEditForm):
@ -838,8 +861,23 @@ class RearPortTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('description',)
nullable_fields = ('description',)
class ModuleBayTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBayTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
max_length=64,
required=False
)
description = forms.CharField(
required=False
)
nullable_fields = ('label', 'position', 'description')
class DeviceBayTemplateBulkEditForm(BulkEditForm): class DeviceBayTemplateBulkEditForm(BulkEditForm):
@ -855,90 +893,125 @@ class DeviceBayTemplateBulkEditForm(BulkEditForm):
required=False required=False
) )
class Meta: nullable_fields = ('label', 'description')
nullable_fields = ('label', 'description')
class InventoryItemTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=InventoryItemTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
max_length=64,
required=False
)
description = forms.CharField(
required=False
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
# #
# Device components # Device components
# #
class ConsolePortBulkEditForm( class ComponentBulkEditForm(NetBoxModelBulkEditForm):
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=ConsolePort.objects.all(),
widget=forms.MultipleHiddenInput()
)
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
class Meta:
nullable_fields = ['label', 'description']
class ConsoleServerPortBulkEditForm(
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=ConsoleServerPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
class Meta:
nullable_fields = ['label', 'description']
class PowerPortBulkEditForm(
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=PowerPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
class Meta:
nullable_fields = ['label', 'description']
class PowerOutletBulkEditForm(
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=PowerOutlet.objects.all(),
widget=forms.MultipleHiddenInput()
)
device = forms.ModelChoiceField( device = forms.ModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
disabled=True, disabled=True,
widget=forms.HiddenInput() widget=forms.HiddenInput()
) )
module = forms.ModelChoiceField(
queryset=Module.objects.all(),
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit module queryset to Modules which belong to the parent Device
if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
self.fields['module'].queryset = Module.objects.filter(device=device)
else:
self.fields['module'].choices = ()
self.fields['module'].widget.attrs['disabled'] = True
class ConsolePortBulkEditForm(
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField( mark_connected = forms.NullBooleanField(
required=False, required=False,
widget=BulkEditNullBooleanSelect widget=BulkEditNullBooleanSelect
) )
class Meta: model = ConsolePort
nullable_fields = ['label', 'type', 'feed_leg', 'power_port', 'description'] fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
)
nullable_fields = ('module', 'label', 'description')
class ConsoleServerPortBulkEditForm(
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
model = ConsoleServerPort
fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
)
nullable_fields = ('module', 'label', 'description')
class PowerPortBulkEditForm(
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
model = PowerPort
fieldsets = (
(None, ('module', 'type', 'label', 'description', 'mark_connected')),
('Power', ('maximum_draw', 'allocated_draw')),
)
nullable_fields = ('module', 'label', 'description')
class PowerOutletBulkEditForm(
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
ComponentBulkEditForm
):
mark_connected = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect
)
model = PowerOutlet
fieldsets = (
(None, ('module', 'type', 'label', 'description', 'mark_connected')),
('Power', ('feed_leg', 'power_port')),
)
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -954,22 +1027,12 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm( class InterfaceBulkEditForm(
form_from_model(Interface, [ form_from_model(Interface, [
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tx_power',
]), ]),
AddRemoveTagsForm, ComponentBulkEditForm
CustomFieldModelBulkEditForm
): ):
pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput()
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
disabled=True,
widget=forms.HiddenInput()
)
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,
widget=BulkEditNullBooleanSelect widget=BulkEditNullBooleanSelect
@ -987,7 +1050,13 @@ class InterfaceBulkEditForm(
required=False, required=False,
query_params={ query_params={
'type': 'lag', 'type': 'lag',
} },
label='LAG'
)
speed = forms.IntegerField(
required=False,
widget=SelectSpeedWidget(),
label='Speed'
) )
mgmt_only = forms.NullBooleanField( mgmt_only = forms.NullBooleanField(
required=False, required=False,
@ -1006,12 +1075,26 @@ class InterfaceBulkEditForm(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False required=False
) )
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
)
class Meta: model = Interface
nullable_fields = [ fieldsets = (
'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', ('Addressing', ('vrf', 'mac_address', 'wwn')),
] ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
)
nullable_fields = (
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
'vrf',
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1075,59 +1158,83 @@ class InterfaceBulkEditForm(
class FrontPortBulkEditForm( class FrontPortBulkEditForm(
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']), form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
AddRemoveTagsForm, ComponentBulkEditForm
CustomFieldModelBulkEditForm
): ):
pk = forms.ModelMultipleChoiceField( model = FrontPort
queryset=FrontPort.objects.all(), fieldsets = (
widget=forms.MultipleHiddenInput() (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
) )
nullable_fields = ('module', 'label', 'description')
class Meta:
nullable_fields = ['label', 'description']
class RearPortBulkEditForm( class RearPortBulkEditForm(
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']), form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
AddRemoveTagsForm, ComponentBulkEditForm
CustomFieldModelBulkEditForm
): ):
pk = forms.ModelMultipleChoiceField( model = RearPort
queryset=RearPort.objects.all(), fieldsets = (
widget=forms.MultipleHiddenInput() (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
) )
nullable_fields = ('module', 'label', 'description')
class Meta:
nullable_fields = ['label', 'description'] class ModuleBayBulkEditForm(
form_from_model(ModuleBay, ['label', 'position', 'description']),
NetBoxModelBulkEditForm
):
model = ModuleBay
fieldsets = (
(None, ('label', 'position', 'description')),
)
nullable_fields = ('label', 'position', 'description')
class DeviceBayBulkEditForm( class DeviceBayBulkEditForm(
form_from_model(DeviceBay, ['label', 'description']), form_from_model(DeviceBay, ['label', 'description']),
AddRemoveTagsForm, NetBoxModelBulkEditForm
CustomFieldModelBulkEditForm
): ):
pk = forms.ModelMultipleChoiceField( model = DeviceBay
queryset=DeviceBay.objects.all(), fieldsets = (
widget=forms.MultipleHiddenInput() (None, ('label', 'description')),
) )
nullable_fields = ('label', 'description')
class Meta:
nullable_fields = ['label', 'description']
class InventoryItemBulkEditForm( class InventoryItemBulkEditForm(
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']), form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
AddRemoveTagsForm, NetBoxModelBulkEditForm
CustomFieldModelBulkEditForm
): ):
pk = forms.ModelMultipleChoiceField( role = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(), queryset=InventoryItemRole.objects.all(),
widget=forms.MultipleHiddenInput() required=False
) )
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
class Meta: model = InventoryItem
nullable_fields = ['label', 'manufacturer', 'part_id', 'description'] fieldsets = (
(None, ('label', 'role', 'manufacturer', 'part_id', 'description')),
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
#
# Device component roles
#
class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
required=False
)
description = forms.CharField(
max_length=200,
required=False
)
model = InventoryItemRole
fieldsets = (
(None, ('color', 'description')),
)
nullable_fields = ('color', 'description')

View File

@ -7,7 +7,8 @@ from django.utils.safestring import mark_safe
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.forms import CustomFieldModelCSVForm from ipam.models import VRF
from netbox.forms import NetBoxModelCSVForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
from virtualization.models import Cluster from virtualization.models import Cluster
@ -24,8 +25,11 @@ __all__ = (
'FrontPortCSVForm', 'FrontPortCSVForm',
'InterfaceCSVForm', 'InterfaceCSVForm',
'InventoryItemCSVForm', 'InventoryItemCSVForm',
'InventoryItemRoleCSVForm',
'LocationCSVForm', 'LocationCSVForm',
'ManufacturerCSVForm', 'ManufacturerCSVForm',
'ModuleCSVForm',
'ModuleBayCSVForm',
'PlatformCSVForm', 'PlatformCSVForm',
'PowerFeedCSVForm', 'PowerFeedCSVForm',
'PowerOutletCSVForm', 'PowerOutletCSVForm',
@ -42,7 +46,7 @@ __all__ = (
) )
class RegionCSVForm(CustomFieldModelCSVForm): class RegionCSVForm(NetBoxModelCSVForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -55,7 +59,7 @@ class RegionCSVForm(CustomFieldModelCSVForm):
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description')
class SiteGroupCSVForm(CustomFieldModelCSVForm): class SiteGroupCSVForm(NetBoxModelCSVForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
@ -68,7 +72,7 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm):
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description')
class SiteCSVForm(CustomFieldModelCSVForm): class SiteCSVForm(NetBoxModelCSVForm):
status = CSVChoiceField( status = CSVChoiceField(
choices=SiteStatusChoices, choices=SiteStatusChoices,
help_text='Operational status' help_text='Operational status'
@ -96,8 +100,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
model = Site model = Site
fields = ( fields = (
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
'contact_email', 'comments',
) )
help_texts = { help_texts = {
'time_zone': mark_safe( 'time_zone': mark_safe(
@ -106,7 +109,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
} }
class LocationCSVForm(CustomFieldModelCSVForm): class LocationCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
@ -133,7 +136,7 @@ class LocationCSVForm(CustomFieldModelCSVForm):
fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description') fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description')
class RackRoleCSVForm(CustomFieldModelCSVForm): class RackRoleCSVForm(NetBoxModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -144,7 +147,7 @@ class RackRoleCSVForm(CustomFieldModelCSVForm):
} }
class RackCSVForm(CustomFieldModelCSVForm): class RackCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name' to_field_name='name'
@ -202,7 +205,7 @@ class RackCSVForm(CustomFieldModelCSVForm):
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
class RackReservationCSVForm(CustomFieldModelCSVForm): class RackReservationCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
@ -252,14 +255,14 @@ class RackReservationCSVForm(CustomFieldModelCSVForm):
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
class ManufacturerCSVForm(CustomFieldModelCSVForm): class ManufacturerCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ('name', 'slug', 'description') fields = ('name', 'slug', 'description')
class DeviceRoleCSVForm(CustomFieldModelCSVForm): class DeviceRoleCSVForm(NetBoxModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -270,7 +273,7 @@ class DeviceRoleCSVForm(CustomFieldModelCSVForm):
} }
class PlatformCSVForm(CustomFieldModelCSVForm): class PlatformCSVForm(NetBoxModelCSVForm):
slug = SlugField() slug = SlugField()
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -284,7 +287,7 @@ class PlatformCSVForm(CustomFieldModelCSVForm):
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description') fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
class BaseDeviceCSVForm(CustomFieldModelCSVForm): class BaseDeviceCSVForm(NetBoxModelCSVForm):
device_role = CSVModelChoiceField( device_role = CSVModelChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='name', to_field_name='name',
@ -400,6 +403,35 @@ class DeviceCSVForm(BaseDeviceCSVForm):
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
class ModuleCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name'
)
module_bay = CSVModelChoiceField(
queryset=ModuleBay.objects.all(),
to_field_name='name'
)
module_type = CSVModelChoiceField(
queryset=ModuleType.objects.all(),
to_field_name='model'
)
class Meta:
model = Module
fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit module_bay queryset by assigned device
params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)
class ChildDeviceCSVForm(BaseDeviceCSVForm): class ChildDeviceCSVForm(BaseDeviceCSVForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -446,7 +478,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
# Device components # Device components
# #
class ConsolePortCSVForm(CustomFieldModelCSVForm): class ConsolePortCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -469,7 +501,7 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm):
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -492,7 +524,7 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
class PowerPortCSVForm(CustomFieldModelCSVForm): class PowerPortCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -510,7 +542,7 @@ class PowerPortCSVForm(CustomFieldModelCSVForm):
) )
class PowerOutletCSVForm(CustomFieldModelCSVForm): class PowerOutletCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -559,7 +591,7 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm):
self.fields['power_port'].queryset = PowerPort.objects.none() self.fields['power_port'].queryset = PowerPort.objects.none()
class InterfaceCSVForm(CustomFieldModelCSVForm): class InterfaceCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -586,11 +618,21 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
help_text='Physical medium' help_text='Physical medium'
) )
duplex = CSVChoiceField(
choices=InterfaceDuplexChoices,
required=False
)
mode = CSVChoiceField( mode = CSVChoiceField(
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
required=False, required=False,
help_text='IEEE 802.1Q operational mode (for L2 interfaces)' help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
) )
vrf = CSVModelChoiceField(
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text='Assigned VRF'
)
rf_role = CSVChoiceField( rf_role = CSVChoiceField(
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
required=False, required=False,
@ -600,8 +642,8 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = Interface model = Interface
fields = ( fields = (
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'mac_address',
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'tx_power', 'rf_channel_width', 'tx_power',
) )
@ -626,7 +668,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
return self.cleaned_data['enabled'] return self.cleaned_data['enabled']
class FrontPortCSVForm(CustomFieldModelCSVForm): class FrontPortCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -674,7 +716,7 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
self.fields['rear_port'].queryset = RearPort.objects.none() self.fields['rear_port'].queryset = RearPort.objects.none()
class RearPortCSVForm(CustomFieldModelCSVForm): class RearPortCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -692,7 +734,18 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
} }
class DeviceBayCSVForm(CustomFieldModelCSVForm): class ModuleBayCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleBay
fields = ('device', 'name', 'label', 'position', 'description')
class DeviceBayCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
@ -738,11 +791,16 @@ class DeviceBayCSVForm(CustomFieldModelCSVForm):
self.fields['installed_device'].queryset = Interface.objects.none() self.fields['installed_device'].queryset = Interface.objects.none()
class InventoryItemCSVForm(CustomFieldModelCSVForm): class InventoryItemCSVForm(NetBoxModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
role = CSVModelChoiceField(
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
@ -758,7 +816,8 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ( fields = (
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -777,7 +836,26 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
self.fields['parent'].queryset = InventoryItem.objects.none() self.fields['parent'].queryset = InventoryItem.objects.none()
class CableCSVForm(CustomFieldModelCSVForm): #
# Device component roles
#
class InventoryItemRoleCSVForm(NetBoxModelCSVForm):
slug = SlugField()
class Meta:
model = InventoryItemRole
fields = ('name', 'slug', 'color', 'description')
help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
}
#
# Cables
#
class CableCSVForm(NetBoxModelCSVForm):
# Termination A # Termination A
side_a_device = CSVModelChoiceField( side_a_device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -878,7 +956,11 @@ class CableCSVForm(CustomFieldModelCSVForm):
return length_unit if length_unit is not None else '' return length_unit if length_unit is not None else ''
class VirtualChassisCSVForm(CustomFieldModelCSVForm): #
# Virtual chassis
#
class VirtualChassisCSVForm(NetBoxModelCSVForm):
master = CSVModelChoiceField( master = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
@ -891,7 +973,11 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
fields = ('name', 'domain', 'master') fields = ('name', 'domain', 'master')
class PowerPanelCSVForm(CustomFieldModelCSVForm): #
# Power
#
class PowerPanelCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
@ -917,7 +1003,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
class PowerFeedCSVForm(CustomFieldModelCSVForm): class PowerFeedCSVForm(NetBoxModelCSVForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',

View File

@ -1,7 +1,7 @@
from circuits.models import Circuit, CircuitTermination, Provider from circuits.models import Circuit, CircuitTermination, Provider
from dcim.models import * from dcim.models import *
from extras.forms import CustomFieldModelForm
from extras.models import Tag from extras.models import Tag
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
@ -18,7 +18,7 @@ __all__ = (
) )
class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm): class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
""" """
Base form for connecting a Cable to a Device component Base form for connecting a Cable to a Device component
""" """
@ -70,10 +70,6 @@ class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
'rack_id': '$termination_b_rack', 'rack_id': '$termination_b_rack',
} }
) )
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = Cable model = Cable
@ -171,7 +167,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
) )
class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm): class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
termination_b_provider = DynamicModelChoiceField( termination_b_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider', label='Provider',
@ -212,10 +208,6 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
'circuit_id': '$termination_b_circuit' 'circuit_id': '$termination_b_circuit'
} }
) )
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta(ConnectCableToDeviceForm.Meta): class Meta(ConnectCableToDeviceForm.Meta):
fields = [ fields = [
@ -229,7 +221,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
return getattr(self.cleaned_data['termination_b_id'], 'pk', None) return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm): class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
termination_b_region = DynamicModelChoiceField( termination_b_region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Region', label='Region',
@ -274,10 +266,6 @@ class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
'power_panel_id': '$termination_b_powerpanel' 'power_panel_id': '$termination_b_powerpanel'
} }
) )
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta(ConnectCableToDeviceForm.Meta): class Meta(ConnectCableToDeviceForm.Meta):
fields = [ fields = [

View File

@ -5,13 +5,13 @@ from django.utils.translation import gettext as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from tenancy.models import * from extras.forms import LocalConfigContextFilterForm
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm from ipam.models import ASN, VRF
from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
) )
from wireless.choices import * from wireless.choices import *
@ -28,8 +28,13 @@ __all__ = (
'InterfaceConnectionFilterForm', 'InterfaceConnectionFilterForm',
'InterfaceFilterForm', 'InterfaceFilterForm',
'InventoryItemFilterForm', 'InventoryItemFilterForm',
'InventoryItemRoleFilterForm',
'LocationFilterForm', 'LocationFilterForm',
'ManufacturerFilterForm', 'ManufacturerFilterForm',
'ModuleFilterForm',
'ModuleFilterForm',
'ModuleBayFilterForm',
'ModuleTypeFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
'PowerConnectionFilterForm', 'PowerConnectionFilterForm',
'PowerFeedFilterForm', 'PowerFeedFilterForm',
@ -48,7 +53,7 @@ __all__ = (
) )
class DeviceComponentFilterForm(CustomFieldModelFilterForm): class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
name = forms.CharField( name = forms.CharField(
required=False required=False
) )
@ -99,13 +104,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
) )
class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region model = Region
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag', 'parent_id')),
['parent_id'], ('Contacts', ('contact', 'contact_role'))
['contact', 'contact_role'], )
]
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -114,13 +118,12 @@ class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup model = SiteGroup
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag', 'parent_id')),
['parent_id'], ('Contacts', ('contact', 'contact_role'))
['contact', 'contact_role'], )
]
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
@ -129,19 +132,17 @@ class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site model = Site
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['status', 'region_id', 'group_id'], ('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
['asn_id'], ('Contacts', ('contact', 'contact_role')),
['contact', 'contact_role'], )
] status = MultipleChoiceField(
status = forms.MultipleChoiceField(
choices=SiteStatusChoices, choices=SiteStatusChoices,
required=False, required=False
widget=StaticSelectMultiple(),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -161,14 +162,14 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModel
tag = TagFilterField(model) tag = TagFilterField(model)
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location model = Location
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_group_id', 'site_id', 'parent_id'], ('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
['contact', 'contact_role'], ('Contacts', ('contact', 'contact_role')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -200,21 +201,21 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldM
tag = TagFilterField(model) tag = TagFilterField(model)
class RackRoleFilterForm(CustomFieldModelFilterForm): class RackRoleFilterForm(NetBoxModelFilterSetForm):
model = RackRole model = RackRole
tag = TagFilterField(model) tag = TagFilterField(model)
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack model = Rack
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_id', 'location_id'], ('Location', ('region_id', 'site_id', 'location_id')),
['status', 'role_id'], ('Function', ('status', 'role_id')),
['type', 'width', 'serial', 'asset_tag'], ('Hardware', ('type', 'width', 'serial', 'asset_tag')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
['contact', 'contact_role'] ('Contacts', ('contact', 'contact_role')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -237,20 +238,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModel
}, },
label=_('Location') label=_('Location')
) )
status = forms.MultipleChoiceField( status = MultipleChoiceField(
choices=RackStatusChoices, choices=RackStatusChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=RackTypeChoices, choices=RackTypeChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
width = forms.MultipleChoiceField( width = MultipleChoiceField(
choices=RackWidthChoices, choices=RackWidthChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
role_id = DynamicModelMultipleChoiceField( role_id = DynamicModelMultipleChoiceField(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
@ -279,14 +277,14 @@ class RackElevationFilterForm(RackFilterForm):
) )
class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation model = RackReservation
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['user_id'], ('User', ('user_id',)),
['region_id', 'site_id', 'location_id'], ('Rack', ('region_id', 'site_id', 'location_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -317,36 +315,40 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer model = Manufacturer
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['contact', 'contact_role'], ('Contacts', ('contact', 'contact_role'))
] )
tag = TagFilterField(model) tag = TagFilterField(model)
class DeviceTypeFilterForm(CustomFieldModelFilterForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType model = DeviceType
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['manufacturer_id', 'subdevice_role', 'airflow'], ('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], ('Components', (
] 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports',
)),
)
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
label=_('Manufacturer') label=_('Manufacturer')
) )
subdevice_role = forms.MultipleChoiceField( part_number = forms.CharField(
choices=add_blank_choice(SubdeviceRoleChoices), required=False
required=False,
widget=StaticSelectMultiple()
) )
airflow = forms.MultipleChoiceField( subdevice_role = MultipleChoiceField(
choices=add_blank_choice(SubdeviceRoleChoices),
required=False
)
airflow = MultipleChoiceField(
choices=add_blank_choice(DeviceAirflowChoices), choices=add_blank_choice(DeviceAirflowChoices),
required=False, required=False
widget=StaticSelectMultiple()
) )
console_ports = forms.NullBooleanField( console_ports = forms.NullBooleanField(
required=False, required=False,
@ -393,12 +395,76 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class DeviceRoleFilterForm(CustomFieldModelFilterForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType
fieldsets = (
(None, ('q', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports',
)),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
)
part_number = forms.CharField(
required=False
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
model = DeviceRole model = DeviceRole
tag = TagFilterField(model) tag = TagFilterField(model)
class PlatformFilterForm(CustomFieldModelFilterForm): class PlatformFilterForm(NetBoxModelFilterSetForm):
model = Platform model = Platform
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -408,20 +474,25 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): class DeviceFilterForm(
LocalConfigContextFilterForm,
TenancyFilterForm,
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
model = Device model = Device
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'], ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
['status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address'], ('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
['manufacturer_id', 'device_type_id', 'platform_id'], ('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
[ ('Contacts', ('contact', 'contact_role')),
'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', ('Components', (
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
], )),
['contact', 'contact_role'], ('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'local_context_data'))
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -484,15 +555,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactM
null_option='None', null_option='None',
label=_('Platform') label=_('Platform')
) )
status = forms.MultipleChoiceField( status = MultipleChoiceField(
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
airflow = forms.MultipleChoiceField( airflow = MultipleChoiceField(
choices=add_blank_choice(DeviceAirflowChoices), choices=add_blank_choice(DeviceAirflowChoices),
required=False, required=False
widget=StaticSelectMultiple()
) )
serial = forms.CharField( serial = forms.CharField(
required=False required=False
@ -563,13 +632,43 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactM
tag = TagFilterField(model) tag = TagFilterField(model)
class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module
fieldsets = (
(None, ('q', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
)
module_type_id = DynamicModelMultipleChoiceField(
queryset=ModuleType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer_id'
},
label=_('Type'),
fetch_trigger='open'
)
serial = forms.CharField(
required=False
)
asset_tag = forms.CharField(
required=False
)
tag = TagFilterField(model)
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis model = VirtualChassis
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_group_id', 'site_id'], ('Location', ('region_id', 'site_group_id', 'site_id')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -592,14 +691,14 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable model = Cable
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['site_id', 'rack_id', 'device_id'], ('Location', ('site_id', 'rack_id', 'device_id')),
['type', 'status', 'color', 'length', 'length_unit'], ('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
['tenant_group_id', 'tenant_id'], ('Tenant', ('tenant_group_id', 'tenant_id')),
] )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -632,15 +731,13 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
}, },
label=_('Device') label=_('Device')
) )
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=add_blank_choice(CableTypeChoices), choices=add_blank_choice(CableTypeChoices),
required=False, required=False
widget=StaticSelect()
) )
status = forms.ChoiceField( status = MultipleChoiceField(
required=False, required=False,
choices=add_blank_choice(LinkStatusChoices), choices=add_blank_choice(LinkStatusChoices)
widget=StaticSelect()
) )
color = ColorField( color = ColorField(
required=False required=False
@ -655,12 +752,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel model = PowerPanel
field_groups = ( fieldsets = (
('q', 'tag'), (None, ('q', 'tag')),
('region_id', 'site_group_id', 'site_id', 'location_id'), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('contact', 'contact_role') ('Contacts', ('contact', 'contact_role')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -693,14 +790,13 @@ class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerFeedFilterForm(CustomFieldModelFilterForm): class PowerFeedFilterForm(NetBoxModelFilterSetForm):
model = PowerFeed model = PowerFeed
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['region_id', 'site_group_id', 'site_id'], ('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
['power_panel_id', 'rack_id'], ('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'], )
]
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -737,10 +833,9 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
}, },
label=_('Rack') label=_('Rack')
) )
status = forms.MultipleChoiceField( status = MultipleChoiceField(
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(PowerFeedTypeChoices), choices=add_blank_choice(PowerFeedTypeChoices),
@ -775,91 +870,93 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
class ConsolePortFilterForm(DeviceComponentFilterForm): class ConsolePortFilterForm(DeviceComponentFilterForm):
model = ConsolePort model = ConsolePort
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type', 'speed'], ('Attributes', ('name', 'label', 'type', 'speed')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
]
type = forms.MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False,
widget=StaticSelectMultiple()
) )
speed = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
speed = MultipleChoiceField(
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
tag = TagFilterField(model) tag = TagFilterField(model)
class ConsoleServerPortFilterForm(DeviceComponentFilterForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
model = ConsoleServerPort model = ConsoleServerPort
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type', 'speed'], ('Attributes', ('name', 'label', 'type', 'speed')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
]
type = forms.MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False,
widget=StaticSelectMultiple()
) )
speed = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
speed = MultipleChoiceField(
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerPortFilterForm(DeviceComponentFilterForm): class PowerPortFilterForm(DeviceComponentFilterForm):
model = PowerPort model = PowerPort
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type'], ('Attributes', ('name', 'label', 'type')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerOutletFilterForm(DeviceComponentFilterForm): class PowerOutletFilterForm(DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type'], ('Attributes', ('name', 'label', 'type')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
tag = TagFilterField(model) tag = TagFilterField(model)
class InterfaceFilterForm(DeviceComponentFilterForm): class InterfaceFilterForm(DeviceComponentFilterForm):
model = Interface model = Interface
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'], ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'], ('Addressing', ('vrf_id', 'mac_address', 'wwn')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
] ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
kind = forms.MultipleChoiceField(
choices=InterfaceKindChoices,
required=False,
widget=StaticSelectMultiple()
) )
type = forms.MultipleChoiceField( kind = MultipleChoiceField(
choices=InterfaceKindChoices,
required=False
)
type = MultipleChoiceField(
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
required=False
)
speed = forms.IntegerField(
required=False, required=False,
widget=StaticSelectMultiple() label='Select Speed',
widget=SelectSpeedWidget(attrs={'readonly': None})
)
duplex = MultipleChoiceField(
choices=InterfaceDuplexChoices,
required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,
@ -881,16 +978,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
required=False, required=False,
label='WWN' label='WWN'
) )
rf_role = forms.MultipleChoiceField( rf_role = MultipleChoiceField(
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
required=False, required=False,
widget=StaticSelectMultiple(),
label='Wireless role' label='Wireless role'
) )
rf_channel = forms.MultipleChoiceField( rf_channel = MultipleChoiceField(
choices=WirelessChannelChoices, choices=WirelessChannelChoices,
required=False, required=False,
widget=StaticSelectMultiple(),
label='Wireless channel' label='Wireless channel'
) )
rf_channel_frequency = forms.IntegerField( rf_channel_frequency = forms.IntegerField(
@ -907,20 +1002,24 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
min_value=0, min_value=0,
max_value=127 max_value=127
) )
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
)
tag = TagFilterField(model) tag = TagFilterField(model)
class FrontPortFilterForm(DeviceComponentFilterForm): class FrontPortFilterForm(DeviceComponentFilterForm):
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type', 'color'], ('Attributes', ('name', 'label', 'type', 'color')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
model = FrontPort model = FrontPort
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=PortTypeChoices, choices=PortTypeChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
color = ColorField( color = ColorField(
required=False required=False
@ -930,15 +1029,14 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
class RearPortFilterForm(DeviceComponentFilterForm): class RearPortFilterForm(DeviceComponentFilterForm):
model = RearPort model = RearPort
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'type', 'color'], ('Attributes', ('name', 'label', 'type', 'color')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
type = forms.MultipleChoiceField( type = MultipleChoiceField(
choices=PortTypeChoices, choices=PortTypeChoices,
required=False, required=False
widget=StaticSelectMultiple()
) )
color = ColorField( color = ColorField(
required=False required=False
@ -946,23 +1044,42 @@ class RearPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'position')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
)
tag = TagFilterField(model)
position = forms.CharField(
required=False
)
class DeviceBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay model = DeviceBay
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label'], ('Attributes', ('name', 'label')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
tag = TagFilterField(model) tag = TagFilterField(model)
class InventoryItemFilterForm(DeviceComponentFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem model = InventoryItem
field_groups = [ fieldsets = (
['q', 'tag'], (None, ('q', 'tag')),
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], ('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
] )
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False,
label=_('Role'),
fetch_trigger='open'
)
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
@ -983,6 +1100,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
#
# Device component roles
#
class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm):
model = InventoryItemRole
tag = TagFilterField(model)
# #
# Connections # Connections
# #

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,25 @@
from django import forms from django import forms
from dcim.choices import *
from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.forms import CustomFieldModelForm, CustomFieldsMixin
from extras.models import Tag from extras.models import Tag
from ipam.models import VLAN from netbox.forms import NetBoxModelForm
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
ExpandableNameField, StaticSelect,
) )
from wireless.choices import *
from .common import InterfaceCommonForm
__all__ = ( __all__ = (
'ConsolePortCreateForm', 'ComponentTemplateCreateForm',
'ConsolePortTemplateCreateForm', 'DeviceComponentCreateForm',
'ConsoleServerPortCreateForm',
'ConsoleServerPortTemplateCreateForm',
'DeviceBayCreateForm',
'DeviceBayTemplateCreateForm',
'FrontPortCreateForm', 'FrontPortCreateForm',
'FrontPortTemplateCreateForm', 'FrontPortTemplateCreateForm',
'InterfaceCreateForm', 'ModularComponentTemplateCreateForm',
'InterfaceTemplateCreateForm', 'ModuleBayCreateForm',
'InventoryItemCreateForm', 'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
'PowerPortCreateForm',
'PowerPortTemplateCreateForm',
'RearPortCreateForm',
'RearPortTemplateCreateForm',
'VirtualChassisCreateForm', 'VirtualChassisCreateForm',
) )
class ComponentForm(BootstrapMixin, forms.Form): class ComponentCreateForm(BootstrapMixin, forms.Form):
""" """
Subclass this form when facilitating the creation of one or more device component or component templates based on Subclass this form when facilitating the creation of one or more device component or component templates based on
a name pattern. a name pattern.
@ -52,18 +36,170 @@ class ComponentForm(BootstrapMixin, forms.Form):
def clean(self): def clean(self):
super().clean() super().clean()
# Validate that the number of components being created from both the name_pattern and label_pattern are equal # Validate that all patterned fields generate an equal number of values
if self.cleaned_data['label_pattern']: patterned_fields = [
name_pattern_count = len(self.cleaned_data['name_pattern']) field_name for field_name in self.fields if field_name.endswith('_pattern')
label_pattern_count = len(self.cleaned_data['label_pattern']) ]
if name_pattern_count != label_pattern_count: pattern_count = len(self.cleaned_data['name_pattern'])
for field_name in patterned_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({ raise forms.ValidationError({
'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however ' field_name: f'The provided pattern specifies {value_count} values, but {pattern_count} are '
f'{label_pattern_count} labels will be generated. These counts must match.' f'expected.'
}, code='label_pattern_mismatch') }, code='label_pattern_mismatch')
class VirtualChassisCreateForm(CustomFieldModelForm): class ComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned only to a DeviceType.
"""
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
)
field_order = ('device_type', 'name_pattern', 'label_pattern')
class ModularComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
"""
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
required=False
)
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False
)
field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
class DeviceComponentCreateForm(ComponentCreateForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
field_order = ('device', 'name_pattern', 'label_pattern')
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
rear_port_set = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'device_type', 'name_pattern', 'label_pattern', 'rear_port_set',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO: This needs better validation
if 'device_type' in self.initial or self.data.get('device_type'):
parent = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
elif 'module_type' in self.initial or self.data.get('module_type'):
parent = ModuleType.objects.get(
pk=self.initial.get('module_type') or self.data.get('module_type')
)
else:
return
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in parent.frontporttemplates.all()
]
# Populate rear port choices
choices = []
rear_ports = parent.rearporttemplates.all()
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
}
class FrontPortCreateForm(DeviceComponentCreateForm):
rear_port_set = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'rear_port_set',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
# Determine which rear port positions are occupied. These will be excluded from the list of available
# mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in device.frontports.all()
]
# Populate rear port choices
choices = []
rear_ports = RearPort.objects.filter(device=device)
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
}
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
position_pattern = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
)
field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern')
class ModuleBayCreateForm(DeviceComponentCreateForm):
position_pattern = ExpandableNameField(
label='Position',
required=False,
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
)
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -107,10 +243,6 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
required=False, required=False,
help_text='Position of the first member device. Increases by one for each additional member.' help_text='Position of the first member device. Increases by one for each additional member.'
) )
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
@ -136,521 +268,3 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
member.save() member.save()
return instance return instance
#
# Component templates
#
class ComponentTemplateCreateForm(ComponentForm):
"""
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
"""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
initial_params={
'device_types': 'device_type'
}
)
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
query_params={
'manufacturer_id': '$manufacturer'
}
)
description = forms.CharField(
required=False
)
class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect()
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect()
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=add_blank_choice(PowerPortTypeChoices),
required=False
)
maximum_draw = forms.IntegerField(
min_value=1,
required=False,
help_text="Maximum power draw (watts)"
)
allocated_draw = forms.IntegerField(
min_value=1,
required=False,
help_text="Allocated power draw (watts)"
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw',
'description',
)
class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices),
required=False
)
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False
)
feed_leg = forms.ChoiceField(
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
widget=StaticSelect()
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
'description',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType
device_type = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=device_type
)
class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=InterfaceTypeChoices,
widget=StaticSelect()
)
mgmt_only = forms.BooleanField(
required=False,
label='Management only'
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description')
class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=PortTypeChoices,
widget=StaticSelect()
)
color = ColorField(
required=False
)
rear_port_set = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
device_type = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in device_type.frontporttemplates.all()
]
# Populate rear port choices
choices = []
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
def clean(self):
super().clean()
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
front_port_count = len(self.cleaned_data['name_pattern'])
rear_port_count = len(self.cleaned_data['rear_port_set'])
if front_port_count != rear_port_count:
raise forms.ValidationError({
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
})
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
}
class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
choices=PortTypeChoices,
widget=StaticSelect(),
)
color = ColorField(
required=False
)
positions = forms.IntegerField(
min_value=REARPORT_POSITIONS_MIN,
max_value=REARPORT_POSITIONS_MAX,
initial=1,
help_text='The number of front ports which may be mapped to each rear port'
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
)
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
#
# Device components
#
class ComponentCreateForm(CustomFieldsMixin, ComponentForm):
"""
Base form for the creation of device components (models subclassed from ComponentModel).
"""
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
description = forms.CharField(
max_length=200,
required=False
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class ConsolePortCreateForm(ComponentCreateForm):
model = ConsolePort
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
widget=StaticSelect()
)
speed = forms.ChoiceField(
choices=add_blank_choice(ConsolePortSpeedChoices),
required=False,
widget=StaticSelect()
)
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
class ConsoleServerPortCreateForm(ComponentCreateForm):
model = ConsoleServerPort
type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
widget=StaticSelect()
)
speed = forms.ChoiceField(
choices=add_blank_choice(ConsolePortSpeedChoices),
required=False,
widget=StaticSelect()
)
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
class PowerPortCreateForm(ComponentCreateForm):
model = PowerPort
type = forms.ChoiceField(
choices=add_blank_choice(PowerPortTypeChoices),
required=False,
widget=StaticSelect()
)
maximum_draw = forms.IntegerField(
min_value=1,
required=False,
help_text="Maximum draw in watts"
)
allocated_draw = forms.IntegerField(
min_value=1,
required=False,
help_text="Allocated draw in watts"
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'tags',
)
class PowerOutletCreateForm(ComponentCreateForm):
model = PowerOutlet
type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
widget=StaticSelect()
)
power_port = forms.ModelChoiceField(
queryset=PowerPort.objects.all(),
required=False
)
feed_leg = forms.ChoiceField(
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPorts which belong to the parent Device
device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
model = Interface
type = forms.ChoiceField(
choices=InterfaceTypeChoices,
widget=StaticSelect(),
)
enabled = forms.BooleanField(
required=False,
initial=True
)
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
bridge = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
lag = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device',
'type': 'lag',
},
label='LAG'
)
mac_address = forms.CharField(
required=False,
label='MAC Address'
)
wwn = forms.CharField(
required=False,
label='WWN'
)
mgmt_only = forms.BooleanField(
required=False,
label='Management only',
help_text='This interface is used only for out-of-band management'
)
mode = forms.ChoiceField(
choices=add_blank_choice(InterfaceModeChoices),
required=False,
widget=StaticSelect()
)
rf_role = forms.ChoiceField(
choices=add_blank_choice(WirelessRoleChoices),
required=False,
widget=StaticSelect(),
label='Wireless role'
)
rf_channel = forms.ChoiceField(
choices=add_blank_choice(WirelessChannelChoices),
required=False,
widget=StaticSelect(),
label='Wireless channel'
)
rf_channel_frequency = forms.DecimalField(
required=False,
label='Channel frequency (MHz)'
)
rf_channel_width = forms.DecimalField(
required=False,
label='Channel width (MHz)'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Untagged VLAN'
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Tagged VLANs'
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
'wwn', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit VLAN choices by device
device_id = self.initial.get('device') or self.data.get('device')
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
class FrontPortCreateForm(ComponentCreateForm):
model = FrontPort
type = forms.ChoiceField(
choices=PortTypeChoices,
widget=StaticSelect(),
)
color = ColorField(
required=False
)
rear_port_set = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
'tags',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
# Determine which rear port positions are occupied. These will be excluded from the list of available
# mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in device.frontports.all()
]
# Populate rear port choices
choices = []
rear_ports = RearPort.objects.filter(device=device)
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port_set'].choices = choices
def clean(self):
super().clean()
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
front_port_count = len(self.cleaned_data['name_pattern'])
rear_port_count = len(self.cleaned_data['rear_port_set'])
if front_port_count != rear_port_count:
raise forms.ValidationError({
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
})
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
}
class RearPortCreateForm(ComponentCreateForm):
model = RearPort
type = forms.ChoiceField(
choices=PortTypeChoices,
widget=StaticSelect(),
)
color = ColorField(
required=False
)
positions = forms.IntegerField(
min_value=REARPORT_POSITIONS_MIN,
max_value=REARPORT_POSITIONS_MAX,
initial=1,
help_text='The number of front ports which may be mapped to each rear port'
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
'tags',
)
class DeviceBayCreateForm(ComponentCreateForm):
model = DeviceBay
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
class InventoryItemCreateForm(ComponentCreateForm):
model = InventoryItem
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
query_params={
'device_id': '$device'
}
)
part_id = forms.CharField(
max_length=50,
required=False,
label='Part ID'
)
serial = forms.CharField(
max_length=50,
required=False,
)
asset_tag = forms.CharField(
max_length=50,
required=False,
)
field_order = (
'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
)

View File

@ -11,6 +11,9 @@ __all__ = (
'DeviceTypeImportForm', 'DeviceTypeImportForm',
'FrontPortTemplateImportForm', 'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm', 'InterfaceTemplateImportForm',
'InventoryItemTemplateImportForm',
'ModuleBayTemplateImportForm',
'ModuleTypeImportForm',
'PowerOutletTemplateImportForm', 'PowerOutletTemplateImportForm',
'PowerPortTemplateImportForm', 'PowerPortTemplateImportForm',
'RearPortTemplateImportForm', 'RearPortTemplateImportForm',
@ -31,31 +34,23 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
] ]
class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleType
fields = ['manufacturer', 'model', 'part_number', 'comments']
# #
# Component template import forms # Component template import forms
# #
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
pass
def __init__(self, device_type, data=None, *args, **kwargs):
# Must pass the parent DeviceType on form initialization
data.update({
'device_type': device_type.pk,
})
super().__init__(data, *args, **kwargs)
def clean_device_type(self):
data = self.cleaned_data['device_type']
# Limit fields referencing other components to the parent DeviceType
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
field.queryset = field.queryset.filter(device_type=data)
return data
class ConsolePortTemplateImportForm(ComponentTemplateImportForm): class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
@ -63,7 +58,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
@ -72,7 +67,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
@ -81,7 +76,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
] ]
@ -95,9 +90,23 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
] ]
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
power_port = self.fields['power_port']
power_port.queryset = power_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
power_port = self.fields['power_port']
power_port.queryset = power_port.queryset.filter(module_type=module_type)
return module_type
class InterfaceTemplateImportForm(ComponentTemplateImportForm): class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField( type = forms.ChoiceField(
@ -107,7 +116,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
] ]
@ -120,10 +129,24 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
to_field_name='name' to_field_name='name'
) )
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
return module_type
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description', 'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
] ]
@ -135,7 +158,16 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'device_type', 'name', 'type', 'positions', 'label', 'description', 'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
]
class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
] ]
@ -146,3 +178,40 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
fields = [ fields = [
'device_type', 'name', 'label', 'description', 'device_type', 'name', 'label', 'description',
] ]
class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
parent = forms.ModelChoiceField(
queryset=InventoryItemTemplate.objects.all(),
required=False
)
role = forms.ModelChoiceField(
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
]
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
parent = self.fields['parent']
parent.queryset = parent.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
parent = self.fields['parent']
parent.queryset = parent.queryset.filter(module_type=module_type)
return module_type

View File

@ -50,12 +50,30 @@ class DCIMQuery(graphene.ObjectType):
inventory_item = ObjectField(InventoryItemType) inventory_item = ObjectField(InventoryItemType)
inventory_item_list = ObjectListField(InventoryItemType) inventory_item_list = ObjectListField(InventoryItemType)
inventory_item_role = ObjectField(InventoryItemRoleType)
inventory_item_role_list = ObjectListField(InventoryItemRoleType)
inventory_item_template = ObjectField(InventoryItemTemplateType)
inventory_item_template_list = ObjectListField(InventoryItemTemplateType)
location = ObjectField(LocationType) location = ObjectField(LocationType)
location_list = ObjectListField(LocationType) location_list = ObjectListField(LocationType)
manufacturer = ObjectField(ManufacturerType) manufacturer = ObjectField(ManufacturerType)
manufacturer_list = ObjectListField(ManufacturerType) manufacturer_list = ObjectListField(ManufacturerType)
module = ObjectField(ModuleType)
module_list = ObjectListField(ModuleType)
module_bay = ObjectField(ModuleBayType)
module_bay_list = ObjectListField(ModuleBayType)
module_bay_template = ObjectField(ModuleBayTemplateType)
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
module_type = ObjectField(ModuleTypeType)
module_type_list = ObjectListField(ModuleTypeType)
platform = ObjectField(PlatformType) platform = ObjectField(PlatformType)
platform_list = ObjectListField(PlatformType) platform_list = ObjectListField(PlatformType)

View File

@ -6,7 +6,7 @@ from extras.graphql.mixins import (
) )
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'CableType', 'CableType',
@ -25,8 +25,14 @@ __all__ = (
'InterfaceType', 'InterfaceType',
'InterfaceTemplateType', 'InterfaceTemplateType',
'InventoryItemType', 'InventoryItemType',
'InventoryItemRoleType',
'InventoryItemTemplateType',
'LocationType', 'LocationType',
'ManufacturerType', 'ManufacturerType',
'ModuleType',
'ModuleBayType',
'ModuleBayTemplateType',
'ModuleTypeType',
'PlatformType', 'PlatformType',
'PowerFeedType', 'PowerFeedType',
'PowerOutletType', 'PowerOutletType',
@ -79,7 +85,7 @@ class ComponentTemplateObjectType(
# Model types # Model types
# #
class CableType(PrimaryObjectType): class CableType(NetBoxObjectType):
class Meta: class Meta:
model = models.Cable model = models.Cable
@ -137,7 +143,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, PrimaryObjectType): class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Device model = models.Device
@ -167,6 +173,14 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.InventoryItemTemplate
fields = '__all__'
filterset_class = filtersets.InventoryItemTemplateFilterSet
class DeviceRoleType(OrganizationalObjectType): class DeviceRoleType(OrganizationalObjectType):
class Meta: class Meta:
@ -175,7 +189,7 @@ class DeviceRoleType(OrganizationalObjectType):
filterset_class = filtersets.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
class DeviceTypeType(PrimaryObjectType): class DeviceTypeType(NetBoxObjectType):
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
@ -238,6 +252,14 @@ class InventoryItemType(ComponentObjectType):
filterset_class = filtersets.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
class InventoryItemRoleType(OrganizationalObjectType):
class Meta:
model = models.InventoryItemRole
fields = '__all__'
filterset_class = filtersets.InventoryItemRoleFilterSet
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType): class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
class Meta: class Meta:
@ -254,6 +276,38 @@ class ManufacturerType(OrganizationalObjectType):
filterset_class = filtersets.ManufacturerFilterSet filterset_class = filtersets.ManufacturerFilterSet
class ModuleType(ComponentObjectType):
class Meta:
model = models.Module
fields = '__all__'
filterset_class = filtersets.ModuleFilterSet
class ModuleBayType(ComponentObjectType):
class Meta:
model = models.ModuleBay
fields = '__all__'
filterset_class = filtersets.ModuleBayFilterSet
class ModuleBayTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ModuleBayTemplate
fields = '__all__'
filterset_class = filtersets.ModuleBayTemplateFilterSet
class ModuleTypeType(NetBoxObjectType):
class Meta:
model = models.ModuleType
fields = '__all__'
filterset_class = filtersets.ModuleTypeFilterSet
class PlatformType(OrganizationalObjectType): class PlatformType(OrganizationalObjectType):
class Meta: class Meta:
@ -262,7 +316,7 @@ class PlatformType(OrganizationalObjectType):
filterset_class = filtersets.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(PrimaryObjectType): class PowerFeedType(NetBoxObjectType):
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
@ -298,7 +352,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class PowerPanelType(PrimaryObjectType): class PowerPanelType(NetBoxObjectType):
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
@ -328,7 +382,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Rack model = models.Rack
@ -342,7 +396,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
return self.outer_unit or None return self.outer_unit or None
class RackReservationType(PrimaryObjectType): class RackReservationType(NetBoxObjectType):
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
@ -382,7 +436,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
filterset_class = filtersets.RegionFilterSet filterset_class = filtersets.RegionFilterSet
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
asn = graphene.Field(BigInt) asn = graphene.Field(BigInt)
class Meta: class Meta:
@ -399,7 +453,7 @@ class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
filterset_class = filtersets.SiteGroupFilterSet filterset_class = filtersets.SiteGroupFilterSet
class VirtualChassisType(PrimaryObjectType): class VirtualChassisType(NetBoxObjectType):
class Meta: class Meta:
model = models.VirtualChassis model = models.VirtualChassis

Some files were not shown because too many files have changed in this diff Show More