mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'feature' into develop
This commit is contained in:
commit
9addde00e3
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -3,10 +3,12 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
node-version: [14.x]
|
||||
python-version: ['3.8', '3.9', '3.10']
|
||||
node-version: ['14.x']
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@ -57,7 +59,6 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage tblib
|
||||
ln -s configuration.testing.py netbox/netbox/configuration.py
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
|
10
.readthedocs.yaml
Normal file
10
.readthedocs.yaml
Normal 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
|
@ -1,6 +1,6 @@
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://github.com/django/django
|
||||
Django<4.0
|
||||
Django
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
@ -82,6 +82,10 @@ markdown-include
|
||||
# https://github.com/squidfunk/mkdocs-material
|
||||
mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings
|
||||
mkdocstrings
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr
|
||||
netaddr
|
||||
@ -113,3 +117,7 @@ svgwrite
|
||||
# Tabular dataset library (for table-based exports)
|
||||
# https://github.com/jazzband/tablib
|
||||
tablib
|
||||
|
||||
# Timezone data (required by django-timezone-field on Python 3.9+)
|
||||
# https://github.com/python/tzdata
|
||||
tzdata
|
||||
|
@ -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
|
||||
|
||||
Default: False
|
||||
|
@ -1,6 +1,11 @@
|
||||
# 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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Default: None
|
||||
|
@ -37,4 +37,5 @@ Once component templates have been created, every new device that you create as
|
||||
{!models/dcim/interfacetemplate.md!}
|
||||
{!models/dcim/frontporttemplate.md!}
|
||||
{!models/dcim/rearporttemplate.md!}
|
||||
{!models/dcim/modulebaytemplate.md!}
|
||||
{!models/dcim/devicebaytemplate.md!}
|
||||
|
@ -17,6 +17,7 @@ Device components represent discrete objects within a device which are used to t
|
||||
{!models/dcim/interface.md!}
|
||||
{!models/dcim/frontport.md!}
|
||||
{!models/dcim/rearport.md!}
|
||||
{!models/dcim/modulebay.md!}
|
||||
{!models/dcim/devicebay.md!}
|
||||
{!models/dcim/inventoryitem.md!}
|
||||
|
||||
|
4
docs/core-functionality/modules.md
Normal file
4
docs/core-functionality/modules.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Modules
|
||||
|
||||
{!models/dcim/moduletype.md!}
|
||||
{!models/dcim/module.md!}
|
@ -1,3 +1,4 @@
|
||||
# Service Mapping
|
||||
|
||||
{!models/ipam/servicetemplate.md!}
|
||||
{!models/ipam/service.md!}
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## 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:
|
||||
|
||||
|
@ -11,17 +11,25 @@ Getting started with NetBox development is pretty straightforward, and should fe
|
||||
|
||||
### 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).
|
||||
|
||||

|
||||
|
||||
Copy the URL provided in the dialog box.
|
||||
|
||||

|
||||
|
||||
You can then clone your GitHub fork locally for development:
|
||||
|
||||
```no-highlight
|
||||
$ git clone https://github.com/youruseraccount/netbox.git
|
||||
$ git clone https://github.com/$username/netbox.git
|
||||
Cloning into 'netbox'...
|
||||
remote: Enumerating objects: 231, done.
|
||||
remote: Counting objects: 100% (231/231), done.
|
||||
remote: Compressing objects: 100% (147/147), done.
|
||||
remote: Total 56705 (delta 134), reused 145 (delta 84), pack-reused 56474
|
||||
Receiving objects: 100% (56705/56705), 27.96 MiB | 34.92 MiB/s, done.
|
||||
Resolving deltas: 100% (44177/44177), done.
|
||||
remote: Enumerating objects: 85949, done.
|
||||
remote: Counting objects: 100% (4672/4672), done.
|
||||
remote: Compressing objects: 100% (1224/1224), done.
|
||||
remote: Total 85949 (delta 3538), reused 4332 (delta 3438), pack-reused 81277
|
||||
Receiving objects: 100% (85949/85949), 55.16 MiB | 44.90 MiB/s, done.
|
||||
Resolving deltas: 100% (68008/68008), done.
|
||||
$ ls netbox/
|
||||
base_requirements.txt contrib docs mkdocs.yml NOTICE requirements.txt upgrade.sh
|
||||
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
|
||||
* `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).
|
||||
|
||||
@ -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`.
|
||||
|
||||
!!! 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:
|
||||
|
||||
@ -85,7 +93,7 @@ Collecting Django==3.1 (from -r requirements.txt (line 1))
|
||||
|
||||
### 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
|
||||
* `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:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py runserver
|
||||
$ ./manage.py runserver
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
November 18, 2020 - 15:52:31
|
||||
Django version 3.1, using settings 'netbox.settings'
|
||||
February 18, 2022 - 20:29:57
|
||||
Django version 4.0.2, using settings 'netbox.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
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
|
||||
|
||||
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
|
||||
$ 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
|
||||
$ 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
|
||||
$ 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
|
||||
|
||||
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.
|
||||
|
@ -8,7 +8,7 @@ Check `base_requirements.txt` for any dependencies pinned to a specific version,
|
||||
|
||||
### 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
|
||||
|
||||
|
@ -4,8 +4,11 @@ The `users.UserConfig` model holds individual preferences for each user in the f
|
||||
|
||||
## Available Preferences
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) |
|
||||
| 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 |
|
||||
| Name | Description |
|
||||
|--------------------------|---------------------------------------------------------------|
|
||||
| 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.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 |
|
||||
|
@ -54,7 +54,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
!!! warning "Python 3.7 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.
|
||||
!!! warning "Python 3.8 or later required"
|
||||
NetBox v3.2 requires Python 3.8, 3.9, or 3.10.
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
@ -17,16 +17,11 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
||||
|
||||
=== "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
|
||||
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
|
||||
python3 -V
|
||||
@ -117,11 +112,11 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
|
||||
|
||||
## 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
|
||||
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:
|
||||
@ -234,10 +229,10 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
||||
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
|
||||
sudo PYTHON=/usr/bin/python3.7 /opt/netbox/upgrade.sh
|
||||
sudo PYTHON=/usr/bin/python3.8 /opt/netbox/upgrade.sh
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
@ -15,7 +15,7 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.7 |
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 10 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
|
@ -10,7 +10,7 @@ NetBox v3.0 and later require the following:
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
| Python | 3.7 |
|
||||
| Python | 3.8 |
|
||||
| PostgreSQL | 10 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
@ -81,10 +81,10 @@ sudo ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! 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
|
||||
sudo PYTHON=/usr/bin/python3.7 ./upgrade.sh
|
||||
sudo PYTHON=/usr/bin/python3.8 ./upgrade.sh
|
||||
```
|
||||
|
||||
This script performs the following actions:
|
||||
|
BIN
docs/media/development/github_fork_button.png
Normal file
BIN
docs/media/development/github_fork_button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/media/development/github_fork_dialog.png
Normal file
BIN
docs/media/development/github_fork_dialog.png
Normal file
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 |
@ -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.
|
||||
|
||||
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.
|
||||
|
@ -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.
|
||||
|
||||
!!! 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.
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 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.
|
||||
|
@ -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.
|
||||
|
||||
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 child device (which must be installed within a device bay)
|
||||
* Neither
|
||||
|
||||
!!! 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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 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
|
||||
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.
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 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.
|
||||
|
3
docs/models/dcim/inventoryitemrole.md
Normal file
3
docs/models/dcim/inventoryitemrole.md
Normal 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.
|
3
docs/models/dcim/inventoryitemtemplate.md
Normal file
3
docs/models/dcim/inventoryitemtemplate.md
Normal 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.
|
5
docs/models/dcim/module.md
Normal file
5
docs/models/dcim/module.md
Normal 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.
|
3
docs/models/dcim/modulebay.md
Normal file
3
docs/models/dcim/modulebay.md
Normal 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.
|
3
docs/models/dcim/modulebaytemplate.md
Normal file
3
docs/models/dcim/modulebaytemplate.md
Normal 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.
|
23
docs/models/dcim/moduletype.md
Normal file
23
docs/models/dcim/moduletype.md
Normal 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).
|
@ -19,6 +19,8 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
|
||||
* JSON: Arbitrary data stored in JSON format
|
||||
* Selection: A selection of one of several pre-defined custom choices
|
||||
* 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
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.
|
||||
|
@ -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>
|
||||
```
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `obj` | The NetBox object being displayed |
|
||||
| `debug` | A boolean indicating whether debugging is enabled |
|
||||
| `request` | The current WSGI request |
|
||||
| `user` | The current user (if authenticated) |
|
||||
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user |
|
||||
| Variable | Description |
|
||||
|-----------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| `object` | The NetBox object being displayed |
|
||||
| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 |
|
||||
| `debug` | A boolean indicating whether debugging is enabled |
|
||||
| `request` | The current WSGI request |
|
||||
| `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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
!!! 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
|
||||
|
||||
@ -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.
|
||||
* **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`.
|
||||
* **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`)
|
||||
* **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.)
|
||||
@ -23,7 +23,7 @@ A webhook is a mechanism for conveying to some external system a change that too
|
||||
|
||||
## 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:
|
||||
|
||||
|
3
docs/models/ipam/servicetemplate.md
Normal file
3
docs/models/ipam/servicetemplate.md
Normal 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.
|
@ -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.
|
||||
|
||||
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.
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 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.
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
!!! 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
|
||||
```
|
30
docs/plugins/development/background-tasks.md
Normal file
30
docs/plugins/development/background-tasks.md
Normal 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
|
||||
```
|
70
docs/plugins/development/filtersets.md
Normal file
70
docs/plugins/development/filtersets.md
Normal 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()
|
||||
```
|
216
docs/plugins/development/forms.md
Normal file
216
docs/plugins/development/forms.md
Normal 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
|
52
docs/plugins/development/graphql-api.md
Normal file
52
docs/plugins/development/graphql-api.md
Normal 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
|
171
docs/plugins/development/index.md
Normal file
171
docs/plugins/development/index.md
Normal 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',
|
||||
]
|
||||
```
|
185
docs/plugins/development/models.md
Normal file
185
docs/plugins/development/models.md
Normal 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
|
||||
)
|
||||
```
|
50
docs/plugins/development/navigation.md
Normal file
50
docs/plugins/development/navigation.md
Normal 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.
|
117
docs/plugins/development/rest-api.md
Normal file
117
docs/plugins/development/rest-api.md
Normal 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.
|
88
docs/plugins/development/tables.md
Normal file
88
docs/plugins/development/tables.md
Normal 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
|
247
docs/plugins/development/templates.md
Normal file
247
docs/plugins/development/templates.md
Normal 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
|
177
docs/plugins/development/views.md
Normal file
177
docs/plugins/development/views.md
Normal 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]
|
||||
```
|
@ -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.
|
||||
|
||||
#### [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)
|
||||
|
||||
* Contact Objects ([#1344](https://github.com/netbox-community/netbox/issues/1344))
|
||||
|
229
docs/release-notes/version-3.2.md
Normal file
229
docs/release-notes/version-3.2.md
Normal 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
|
@ -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
|
32
mkdocs.yml
32
mkdocs.yml
@ -18,6 +18,23 @@ theme:
|
||||
toggle:
|
||||
icon: material/lightbulb
|
||||
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:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
@ -62,6 +79,7 @@ nav:
|
||||
- Sites and Racks: 'core-functionality/sites-and-racks.md'
|
||||
- Devices and Cabling: 'core-functionality/devices.md'
|
||||
- Device Types: 'core-functionality/device-types.md'
|
||||
- Modules: 'core-functionality/modules.md'
|
||||
- Virtualization: 'core-functionality/virtualization.md'
|
||||
- Service Mapping: 'core-functionality/services.md'
|
||||
- Circuits: 'core-functionality/circuits.md'
|
||||
@ -86,7 +104,18 @@ nav:
|
||||
- Webhooks: 'additional-features/webhooks.md'
|
||||
- Plugins:
|
||||
- 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:
|
||||
- Authentication: 'administration/authentication.md'
|
||||
- Permissions: 'administration/permissions.md'
|
||||
@ -115,6 +144,7 @@ nav:
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- Release Notes:
|
||||
- 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.0: 'release-notes/version-3.0.md'
|
||||
- Version 2.11: 'release-notes/version-2.11.md'
|
||||
|
@ -4,8 +4,10 @@ from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import LinkTerminationSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from ipam.models import ASN
|
||||
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 .nested_serializers import *
|
||||
|
||||
@ -14,15 +16,23 @@ from .nested_serializers import *
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(PrimaryModelSerializer):
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'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
|
||||
#
|
||||
|
||||
class ProviderNetworkSerializer(PrimaryModelSerializer):
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@ -46,7 +56,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(PrimaryModelSerializer):
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
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')
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
|
@ -1,8 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CircuitsRootView
|
||||
|
||||
# Providers
|
||||
|
@ -3,8 +3,7 @@ from rest_framework.routers import APIRootView
|
||||
from circuits import filtersets
|
||||
from circuits.models import *
|
||||
from dcim.api.views import PassThroughPortMixin
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
@ -21,8 +20,8 @@ class CircuitsRootView(APIRootView):
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderViewSet(CustomFieldModelViewSet):
|
||||
queryset = Provider.objects.prefetch_related('tags').annotate(
|
||||
class ProviderViewSet(NetBoxModelViewSet):
|
||||
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'provider')
|
||||
)
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
@ -33,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
# Circuit Types
|
||||
#
|
||||
|
||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||
class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
@ -45,7 +44,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
class CircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
@ -57,7 +56,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', 'provider_network', 'cable'
|
||||
)
|
||||
@ -70,7 +69,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkViewSet(CustomFieldModelViewSet):
|
||||
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
|
||||
#
|
||||
|
||||
class CircuitStatusChoices(ChoiceSet):
|
||||
key = 'Circuit.status'
|
||||
|
||||
STATUS_DEPROVISIONING = 'deprovisioning'
|
||||
STATUS_ACTIVE = 'active'
|
||||
@ -14,23 +15,14 @@ class CircuitStatusChoices(ChoiceSet):
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_DECOMMISSIONED = 'decommissioned'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_PROVISIONING, 'Provisioning'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_OFFLINE, 'Offline'),
|
||||
(STATUS_DEPROVISIONING, 'Deprovisioning'),
|
||||
(STATUS_DECOMMISSIONED, 'Decommissioned'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_DEPROVISIONING: 'warning',
|
||||
STATUS_ACTIVE: 'success',
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_PROVISIONING: 'primary',
|
||||
STATUS_OFFLINE: 'danger',
|
||||
STATUS_DECOMMISSIONED: 'secondary',
|
||||
}
|
||||
CHOICES = [
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_PROVISIONING, 'Provisioning', 'blue'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_OFFLINE, 'Offline', 'red'),
|
||||
(STATUS_DEPROVISIONING, 'Deprovisioning', 'yellow'),
|
||||
(STATUS_DECOMMISSIONED, 'Decommissioned', 'gray'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
@ -3,8 +3,8 @@ from django.db.models import Q
|
||||
|
||||
from dcim.filtersets import CableTerminationFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
@ -19,11 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
@ -61,7 +57,11 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
asn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns',
|
||||
queryset=ASN.objects.all(),
|
||||
label='ASN (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@ -79,11 +79,7 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
@ -94,35 +90,30 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ['id', 'name', 'service_id', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(service_id__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
@ -189,7 +180,6 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
|
@ -1,10 +1,15 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
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 utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect
|
||||
from utilities.forms import (
|
||||
add_blank_choice, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
|
||||
StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@ -14,14 +19,15 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
label='ASN (legacy)'
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
@ -47,23 +53,27 @@ class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
service_id = forms.CharField(
|
||||
max_length=100,
|
||||
required=False,
|
||||
label='Service ID'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
@ -71,31 +81,29 @@ class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
(None, ('provider', 'service_id', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'service_id', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
(None, ('description',)),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False
|
||||
@ -127,7 +135,10 @@ class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
(None, ('type', 'provider', 'status', 'tenant', 'commit_rate', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from extras.forms import CustomFieldModelCSVForm
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
|
||||
@ -12,7 +12,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
class ProviderCSVForm(NetBoxModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -22,7 +22,7 @@ class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
class ProviderNetworkCSVForm(NetBoxModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -32,11 +32,11 @@ class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
'provider', 'name', 'service_id', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
class CircuitTypeCSVForm(NetBoxModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -47,7 +47,7 @@ class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||
class CircuitCSVForm(NetBoxModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
|
@ -4,9 +4,10 @@ from django.utils.translation import gettext as _
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
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 utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
@ -16,14 +17,14 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Provider
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['asn'],
|
||||
['contact', 'contact_role']
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('ASN', ('asn',)),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -45,40 +46,49 @@ class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN')
|
||||
label=_('ASN (legacy)')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
label=_('ASNs')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(CustomFieldModelFilterForm):
|
||||
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('provider_id',),
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('provider_id', 'service_id')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
service_id = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTypeFilterForm(CustomFieldModelFilterForm):
|
||||
class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitType
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Circuit
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['provider_id', 'provider_network_id'],
|
||||
['type_id', 'status', 'commit_rate'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['contact', 'contact_role']
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Provider', ('provider_id', 'provider_network_id')),
|
||||
('Attributes', ('type_id', 'status', 'commit_rate')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
@ -97,10 +107,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldMo
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
|
@ -1,9 +1,10 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
@ -19,23 +20,25 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderForm(CustomFieldModelForm):
|
||||
class ProviderForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'asns', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
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 = {
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
@ -53,32 +56,25 @@ class ProviderForm(CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkForm(CustomFieldModelForm):
|
||||
class ProviderNetworkForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
|
||||
fieldsets = (
|
||||
('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
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()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
@ -87,7 +83,7 @@ class CircuitTypeForm(CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
@ -95,9 +91,10 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -106,10 +103,6 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'commit_rate': "Committed rate",
|
||||
@ -122,6 +115,19 @@ class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
|
||||
|
||||
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(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -152,8 +158,8 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected',
|
||||
'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
@ -161,12 +167,7 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'term_side': StaticSelect(),
|
||||
'port_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)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType
|
||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
@ -18,7 +18,7 @@ class CircuitTerminationType(ObjectType):
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
class CircuitType(PrimaryObjectType):
|
||||
class CircuitType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
@ -34,7 +34,7 @@ class CircuitTypeType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
class ProviderType(PrimaryObjectType):
|
||||
class ProviderType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Provider
|
||||
@ -42,7 +42,7 @@ class ProviderType(PrimaryObjectType):
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
class ProviderNetworkType(PrimaryObjectType):
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderNetwork
|
||||
|
16
netbox/circuits/migrations/0032_provider_service_id.py
Normal file
16
netbox/circuits/migrations/0032_provider_service_id.py
Normal 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),
|
||||
),
|
||||
]
|
44
netbox/circuits/migrations/0033_standardize_id_fields.py
Normal file
44
netbox/circuits/migrations/0033_standardize_id_fields.py
Normal 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),
|
||||
),
|
||||
]
|
38
netbox/circuits/migrations/0034_created_datetimefield.py
Normal file
38
netbox/circuits/migrations/0034_created_datetimefield.py
Normal 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),
|
||||
),
|
||||
]
|
19
netbox/circuits/migrations/0035_provider_asns.py
Normal file
19
netbox/circuits/migrations/0035_provider_asns.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -5,8 +5,8 @@ from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from dcim.models import LinkTermination
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, NetBoxModel
|
||||
from netbox.models.features import WebhooksMixin
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@ -15,7 +15,6 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class CircuitType(OrganizationalModel):
|
||||
"""
|
||||
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])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Circuit(PrimaryModel):
|
||||
class Circuit(NetBoxModel):
|
||||
"""
|
||||
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
|
||||
@ -134,12 +132,11 @@ class Circuit(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def get_status_class(self):
|
||||
return CircuitStatusChoices.CSS_CLASSES.get(self.status)
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
||||
class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
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.")
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Circuit
|
||||
try:
|
||||
circuit = self.circuit
|
||||
except Circuit.DoesNotExist:
|
||||
# Parent circuit has been deleted
|
||||
circuit = None
|
||||
return super().to_objectchange(action, related_object=circuit)
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.circuit
|
||||
return objectchange
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
|
@ -3,9 +3,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from netbox.models import NetBoxModel
|
||||
|
||||
__all__ = (
|
||||
'ProviderNetwork',
|
||||
@ -13,8 +11,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Provider(PrimaryModel):
|
||||
class Provider(NetBoxModel):
|
||||
"""
|
||||
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.
|
||||
@ -33,6 +30,11 @@ class Provider(PrimaryModel):
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
asns = models.ManyToManyField(
|
||||
to='ipam.ASN',
|
||||
related_name='providers',
|
||||
blank=True
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
@ -73,8 +75,7 @@ class Provider(PrimaryModel):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
class ProviderNetwork(NetBoxModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
unimportant to the user.
|
||||
@ -87,6 +88,11 @@ class ProviderNetwork(PrimaryModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='networks'
|
||||
)
|
||||
service_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
verbose_name='Service ID'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
|
@ -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',
|
||||
)
|
3
netbox/circuits/tables/__init__.py
Normal file
3
netbox/circuits/tables/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .circuits import *
|
||||
from .columns import *
|
||||
from .providers import *
|
77
netbox/circuits/tables/circuits.py
Normal file
77
netbox/circuits/tables/circuits.py
Normal 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',
|
||||
)
|
21
netbox/circuits/tables/columns.py
Normal file
21
netbox/circuits/tables/columns.py
Normal 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
|
67
netbox/circuits/tables/providers.py
Normal file
67
netbox/circuits/tables/providers.py
Normal 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')
|
@ -3,6 +3,7 @@ from django.urls import reverse
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
|
||||
@ -18,20 +19,6 @@ class AppTest(APITestCase):
|
||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
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 = {
|
||||
'asn': 1234,
|
||||
}
|
||||
@ -39,6 +26,12 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
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 = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
@ -46,6 +39,24 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
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):
|
||||
model = CircuitType
|
||||
|
@ -4,6 +4,7 @@ from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
@ -15,6 +16,14 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
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 = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
|
||||
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.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0]])
|
||||
providers[1].asns.set([asns[1]])
|
||||
providers[2].asns.set([asns[2]])
|
||||
|
||||
regions = (
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
@ -70,10 +82,15 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['provider-1', 'provider-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']}
|
||||
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):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Interface, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||
|
||||
|
||||
@ -15,11 +16,21 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@classmethod
|
||||
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 2', slug='provider-2', asn=65002),
|
||||
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')
|
||||
|
||||
@ -27,6 +38,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'name': 'Provider X',
|
||||
'slug': 'provider-x',
|
||||
'asn': 65123,
|
||||
'asns': [asns[6].pk, asns[7].pk],
|
||||
'account': '1234',
|
||||
'portal_url': 'http://example.com/portal',
|
||||
'noc_contact': 'noc@example.com',
|
||||
@ -218,6 +230,7 @@ class CircuitTerminationTestCase(
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
cls.form_data = {
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': 'A',
|
||||
'site': sites[2].pk,
|
||||
'description': 'New description',
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from dcim.views import CableCreateView, PathTraceView
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from utilities.views import SlugRedirectView
|
||||
from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
|
||||
from . import views
|
||||
from .models import *
|
||||
|
||||
@ -16,7 +15,6 @@ urlpatterns = [
|
||||
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
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>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
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'),
|
||||
|
||||
# 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>/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}),
|
||||
|
@ -5,10 +5,8 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import CircuitTerminationSideChoices
|
||||
from .models import *
|
||||
|
||||
|
||||
@ -35,7 +33,7 @@ class ProviderView(generic.ObjectView):
|
||||
'type', 'tenant', 'terminations__site'
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
|
||||
paginate_table(circuits_table, request)
|
||||
circuits_table.configure(request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
@ -44,7 +42,7 @@ class ProviderView(generic.ObjectView):
|
||||
|
||||
class ProviderEditView(generic.ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderForm
|
||||
form = forms.ProviderForm
|
||||
|
||||
|
||||
class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
@ -96,7 +94,7 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
'type', 'tenant', 'terminations__site'
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
paginate_table(circuits_table, request)
|
||||
circuits_table.configure(request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
@ -105,7 +103,7 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
|
||||
class ProviderNetworkEditView(generic.ObjectEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkForm
|
||||
form = forms.ProviderNetworkForm
|
||||
|
||||
|
||||
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
@ -150,7 +148,7 @@ class CircuitTypeView(generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
||||
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
|
||||
paginate_table(circuits_table, request)
|
||||
circuits_table.configure(request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
@ -159,7 +157,7 @@ class CircuitTypeView(generic.ObjectView):
|
||||
|
||||
class CircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeForm
|
||||
form = forms.CircuitTypeForm
|
||||
|
||||
|
||||
class CircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
@ -207,7 +205,7 @@ class CircuitView(generic.ObjectView):
|
||||
|
||||
class CircuitEditView(generic.ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitForm
|
||||
form = forms.CircuitForm
|
||||
|
||||
|
||||
class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
@ -317,17 +315,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
model_form = forms.CircuitTerminationForm
|
||||
form = forms.CircuitTerminationForm
|
||||
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):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
|
@ -4,6 +4,7 @@ from dcim import models
|
||||
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'ComponentNestedModuleSerializer',
|
||||
'NestedCableSerializer',
|
||||
'NestedConsolePortSerializer',
|
||||
'NestedConsolePortTemplateSerializer',
|
||||
@ -19,7 +20,13 @@ __all__ = [
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedInterfaceTemplateSerializer',
|
||||
'NestedInventoryItemSerializer',
|
||||
'NestedInventoryItemRoleSerializer',
|
||||
'NestedInventoryItemTemplateSerializer',
|
||||
'NestedManufacturerSerializer',
|
||||
'NestedModuleBaySerializer',
|
||||
'NestedModuleBayTemplateSerializer',
|
||||
'NestedModuleSerializer',
|
||||
'NestedModuleTypeSerializer',
|
||||
'NestedPlatformSerializer',
|
||||
'NestedPowerFeedSerializer',
|
||||
'NestedPowerOutletSerializer',
|
||||
@ -117,7 +124,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
# Device/module types
|
||||
#
|
||||
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
@ -139,6 +146,20 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||
|
||||
@ -195,6 +216,14 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||
|
||||
@ -203,6 +232,15 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
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
|
||||
#
|
||||
@ -235,6 +273,37 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
@ -298,6 +367,15 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
@ -317,6 +395,15 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
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
|
||||
#
|
||||
|
@ -6,11 +6,13 @@ from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
from dcim.choices import *
|
||||
from dcim.constants 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 netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import (
|
||||
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from netbox.config import ConfigItem
|
||||
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')
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
@ -132,10 +134,10 @@ class SiteSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_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')
|
||||
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')
|
||||
site = NestedSiteSerializer()
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
@ -210,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
return obj['name']
|
||||
|
||||
|
||||
class RackReservationSerializer(PrimaryModelSerializer):
|
||||
class RackReservationSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
rack = NestedRackSerializer()
|
||||
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')
|
||||
devicetype_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')
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
@ -418,11 +449,45 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
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
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(PrimaryModelSerializer):
|
||||
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_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')
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=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')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
@ -491,6 +556,20 @@ class DeviceSerializer(PrimaryModelSerializer):
|
||||
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):
|
||||
config_context = serializers.SerializerMethodField()
|
||||
|
||||
@ -515,9 +594,13 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
@ -533,15 +616,19 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
@ -557,15 +644,19 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
allow_blank=True,
|
||||
@ -587,15 +678,20 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
allow_blank=True,
|
||||
@ -606,20 +702,26 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=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_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
@ -629,6 +731,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
||||
wireless_lans = SerializedPKRelatedField(
|
||||
@ -643,12 +746,12 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu',
|
||||
'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link',
|
||||
'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||
'count_fhrp_groups', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
|
||||
'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoint',
|
||||
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@ -665,16 +768,20 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||
class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
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',
|
||||
'last_updated', '_occupied',
|
||||
]
|
||||
@ -691,9 +798,13 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'label']
|
||||
|
||||
|
||||
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||
class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
module = ComponentNestedModuleSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@ -701,13 +812,26 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
@ -720,22 +844,50 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
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)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'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
|
||||
#
|
||||
|
||||
class CableSerializer(PrimaryModelSerializer):
|
||||
class CableSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
termination_a_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
@ -849,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
@ -866,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelSerializer(PrimaryModelSerializer):
|
||||
class PowerPanelSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
site = NestedSiteSerializer()
|
||||
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')
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
|
@ -1,8 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.DCIMRootView
|
||||
|
||||
# Sites
|
||||
@ -16,9 +16,10 @@ router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
router.register('rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
# Device types
|
||||
# Device/module types
|
||||
router.register('manufacturers', views.ManufacturerViewSet)
|
||||
router.register('device-types', views.DeviceTypeViewSet)
|
||||
router.register('module-types', views.ModuleTypeViewSet)
|
||||
|
||||
# Device type components
|
||||
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('front-port-templates', views.FrontPortTemplateViewSet)
|
||||
router.register('rear-port-templates', views.RearPortTemplateViewSet)
|
||||
router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
|
||||
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('platforms', views.PlatformViewSet)
|
||||
router.register('devices', views.DeviceViewSet)
|
||||
router.register('modules', views.ModuleViewSet)
|
||||
|
||||
# Device components
|
||||
router.register('console-ports', views.ConsolePortViewSet)
|
||||
@ -43,9 +47,13 @@ router.register('power-outlets', views.PowerOutletViewSet)
|
||||
router.register('interfaces', views.InterfaceViewSet)
|
||||
router.register('front-ports', views.FrontPortViewSet)
|
||||
router.register('rear-ports', views.RearPortViewSet)
|
||||
router.register('module-bays', views.ModuleBayViewSet)
|
||||
router.register('device-bays', views.DeviceBayViewSet)
|
||||
router.register('inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Device component roles
|
||||
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
||||
|
||||
# Cables
|
||||
router.register('cables', views.CableViewSet)
|
||||
|
||||
|
@ -14,12 +14,12 @@ from rest_framework.viewsets import ViewSet
|
||||
from circuits.models import Circuit
|
||||
from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from extras.api.views import ConfigContextQuerySetMixin
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.exceptions import ServiceUnavailable
|
||||
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 utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related
|
||||
@ -103,7 +103,7 @@ class PassThroughPortMixin(object):
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionViewSet(CustomFieldModelViewSet):
|
||||
class RegionViewSet(NetBoxModelViewSet):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
@ -119,7 +119,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
class SiteGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
@ -135,7 +135,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteViewSet(CustomFieldModelViewSet):
|
||||
class SiteViewSet(NetBoxModelViewSet):
|
||||
queryset = Site.objects.prefetch_related(
|
||||
'region', 'tenant', 'asns', 'tags'
|
||||
).annotate(
|
||||
@ -154,7 +154,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
# Locations
|
||||
#
|
||||
|
||||
class LocationViewSet(CustomFieldModelViewSet):
|
||||
class LocationViewSet(NetBoxModelViewSet):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
@ -176,7 +176,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
class RackRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
@ -188,7 +188,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackViewSet(CustomFieldModelViewSet):
|
||||
class RackViewSet(NetBoxModelViewSet):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'location', 'role', 'tenant', 'tags'
|
||||
).annotate(
|
||||
@ -250,7 +250,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationViewSet(ModelViewSet):
|
||||
class RackReservationViewSet(NetBoxModelViewSet):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filterset_class = filtersets.RackReservationFilterSet
|
||||
@ -260,7 +260,7 @@ class RackReservationViewSet(ModelViewSet):
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||
class ManufacturerViewSet(NetBoxModelViewSet):
|
||||
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||
devicetype_count=count_related(DeviceType, '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(
|
||||
device_count=count_related(Device, 'device_type')
|
||||
)
|
||||
@ -283,63 +283,84 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
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
|
||||
#
|
||||
|
||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerPortTemplateViewSet(ModelViewSet):
|
||||
class PowerPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerPortTemplateSerializer
|
||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||
|
||||
|
||||
class InterfaceTemplateViewSet(ModelViewSet):
|
||||
class InterfaceTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.InterfaceTemplateSerializer
|
||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
|
||||
class FrontPortTemplateViewSet(ModelViewSet):
|
||||
class FrontPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.FrontPortTemplateSerializer
|
||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||
|
||||
|
||||
class RearPortTemplateViewSet(ModelViewSet):
|
||||
class RearPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.RearPortTemplateSerializer
|
||||
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')
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
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
|
||||
#
|
||||
|
||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
class DeviceRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||
@ -352,7 +373,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformViewSet(CustomFieldModelViewSet):
|
||||
class PlatformViewSet(NetBoxModelViewSet):
|
||||
queryset = Platform.objects.prefetch_related('tags').annotate(
|
||||
device_count=count_related(Device, '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(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||
@ -511,83 +532,120 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
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
|
||||
#
|
||||
|
||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filtersets.ConsolePortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
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
|
||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filtersets.PowerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filterset_class = filtersets.PowerOutletFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
|
||||
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
|
||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = FrontPort.objects.prefetch_related(
|
||||
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
|
||||
)
|
||||
serializer_class = serializers.FrontPortSerializer
|
||||
filterset_class = filtersets.FrontPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
|
||||
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = RearPort.objects.prefetch_related(
|
||||
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
|
||||
)
|
||||
serializer_class = serializers.RearPortSerializer
|
||||
filterset_class = filtersets.RearPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class DeviceBayViewSet(ModelViewSet):
|
||||
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
|
||||
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||
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
|
||||
filterset_class = filtersets.DeviceBayFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class InventoryItemViewSet(ModelViewSet):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
|
||||
class InventoryItemViewSet(NetBoxModelViewSet):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
|
||||
serializer_class = serializers.InventoryItemSerializer
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
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
|
||||
#
|
||||
|
||||
class CableViewSet(ModelViewSet):
|
||||
class CableViewSet(NetBoxModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'termination_a', 'termination_b'
|
||||
@ -600,7 +658,7 @@ class CableViewSet(ModelViewSet):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisViewSet(ModelViewSet):
|
||||
class VirtualChassisViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
@ -613,7 +671,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelViewSet(ModelViewSet):
|
||||
class PowerPanelViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
@ -627,7 +685,7 @@ class PowerPanelViewSet(ModelViewSet):
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
||||
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerFeed.objects.prefetch_related(
|
||||
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
|
||||
#
|
||||
|
||||
class SiteStatusChoices(ChoiceSet):
|
||||
key = 'Site.status'
|
||||
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_STAGING = 'staging'
|
||||
@ -13,21 +14,13 @@ class SiteStatusChoices(ChoiceSet):
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
STATUS_RETIRED = 'retired'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_STAGING, 'Staging'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning'),
|
||||
(STATUS_RETIRED, 'Retired'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_STAGING: 'primary',
|
||||
STATUS_ACTIVE: 'success',
|
||||
STATUS_DECOMMISSIONING: 'warning',
|
||||
STATUS_RETIRED: 'danger',
|
||||
}
|
||||
CHOICES = [
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_STAGING, 'Staging', 'blue'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||
(STATUS_RETIRED, 'Retired', 'red'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@ -67,6 +60,7 @@ class RackWidthChoices(ChoiceSet):
|
||||
|
||||
|
||||
class RackStatusChoices(ChoiceSet):
|
||||
key = 'Rack.status'
|
||||
|
||||
STATUS_RESERVED = 'reserved'
|
||||
STATUS_AVAILABLE = 'available'
|
||||
@ -74,21 +68,13 @@ class RackStatusChoices(ChoiceSet):
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_DEPRECATED = 'deprecated'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_RESERVED, 'Reserved'),
|
||||
(STATUS_AVAILABLE, 'Available'),
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_DEPRECATED, 'Deprecated'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_RESERVED: 'warning',
|
||||
STATUS_AVAILABLE: 'success',
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_ACTIVE: 'primary',
|
||||
STATUS_DEPRECATED: 'danger',
|
||||
}
|
||||
CHOICES = [
|
||||
(STATUS_RESERVED, 'Reserved', 'yellow'),
|
||||
(STATUS_AVAILABLE, 'Available', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_ACTIVE, 'Active', 'blue'),
|
||||
(STATUS_DEPRECATED, 'Deprecated', 'red'),
|
||||
]
|
||||
|
||||
|
||||
class RackDimensionUnitChoices(ChoiceSet):
|
||||
@ -144,6 +130,7 @@ class DeviceFaceChoices(ChoiceSet):
|
||||
|
||||
|
||||
class DeviceStatusChoices(ChoiceSet):
|
||||
key = 'Device.status'
|
||||
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_ACTIVE = 'active'
|
||||
@ -153,25 +140,15 @@ class DeviceStatusChoices(ChoiceSet):
|
||||
STATUS_INVENTORY = 'inventory'
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_OFFLINE, 'Offline'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_STAGED, 'Staged'),
|
||||
(STATUS_FAILED, 'Failed'),
|
||||
(STATUS_INVENTORY, 'Inventory'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_OFFLINE: 'warning',
|
||||
STATUS_ACTIVE: 'success',
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_STAGED: 'primary',
|
||||
STATUS_FAILED: 'danger',
|
||||
STATUS_INVENTORY: 'secondary',
|
||||
STATUS_DECOMMISSIONING: 'warning',
|
||||
}
|
||||
CHOICES = [
|
||||
(STATUS_OFFLINE, 'Offline', 'gray'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_STAGED, 'Staged', 'blue'),
|
||||
(STATUS_FAILED, 'Failed', 'red'),
|
||||
(STATUS_INVENTORY, 'Inventory', 'purple'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
|
||||
MODE_ACCESS = 'access'
|
||||
@ -1164,17 +1154,11 @@ class LinkStatusChoices(ChoiceSet):
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_CONNECTED, 'Connected'),
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning'),
|
||||
(STATUS_CONNECTED, 'Connected', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'blue'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_CONNECTED: 'success',
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_DECOMMISSIONING: 'warning',
|
||||
}
|
||||
|
||||
|
||||
class CableLengthUnitChoices(ChoiceSet):
|
||||
|
||||
@ -1203,25 +1187,19 @@ class CableLengthUnitChoices(ChoiceSet):
|
||||
#
|
||||
|
||||
class PowerFeedStatusChoices(ChoiceSet):
|
||||
key = 'PowerFeed.status'
|
||||
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_FAILED = 'failed'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_OFFLINE, 'Offline'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_FAILED, 'Failed'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
STATUS_OFFLINE: 'warning',
|
||||
STATUS_ACTIVE: 'success',
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_FAILED: 'danger',
|
||||
}
|
||||
CHOICES = [
|
||||
(STATUS_OFFLINE, 'Offline', 'gray'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_PLANNED, 'Planned', 'blue'),
|
||||
(STATUS_FAILED, 'Failed', 'red'),
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedTypeChoices(ChoiceSet):
|
||||
@ -1230,15 +1208,10 @@ class PowerFeedTypeChoices(ChoiceSet):
|
||||
TYPE_REDUNDANT = 'redundant'
|
||||
|
||||
CHOICES = (
|
||||
(TYPE_PRIMARY, 'Primary'),
|
||||
(TYPE_REDUNDANT, 'Redundant'),
|
||||
(TYPE_PRIMARY, 'Primary', 'green'),
|
||||
(TYPE_REDUNDANT, 'Redundant', 'cyan'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
TYPE_PRIMARY: 'success',
|
||||
TYPE_REDUNDANT: 'info',
|
||||
}
|
||||
|
||||
|
||||
class PowerFeedSupplyChoices(ChoiceSet):
|
||||
|
||||
|
@ -50,16 +50,43 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
|
||||
#
|
||||
# PowerFeeds
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
POWERFEED_VOLTAGE_DEFAULT = 120
|
||||
|
||||
POWERFEED_AMPERAGE_DEFAULT = 20
|
||||
|
||||
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
|
||||
#
|
||||
|
@ -1,11 +1,10 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from extras.filters import TagFilter
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from ipam.models import ASN
|
||||
from ipam.models import ASN, VRF
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from tenancy.models import *
|
||||
@ -39,8 +38,14 @@ __all__ = (
|
||||
'InterfaceFilterSet',
|
||||
'InterfaceTemplateFilterSet',
|
||||
'InventoryItemFilterSet',
|
||||
'InventoryItemRoleFilterSet',
|
||||
'InventoryItemTemplateFilterSet',
|
||||
'LocationFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'ModuleBayFilterSet',
|
||||
'ModuleBayTemplateFilterSet',
|
||||
'ModuleFilterSet',
|
||||
'ModuleTypeFilterSet',
|
||||
'PathEndpointFilterSet',
|
||||
'PlatformFilterSet',
|
||||
'PowerConnectionFilterSet',
|
||||
@ -73,7 +78,6 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Parent region (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
@ -91,18 +95,13 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Parent site group (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=SiteStatusChoices,
|
||||
null_value=None
|
||||
@ -131,19 +130,23 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
to_field_name='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(
|
||||
field_name='asns',
|
||||
queryset=ASN.objects.all(),
|
||||
label='AS (ID)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'description'
|
||||
]
|
||||
fields = (
|
||||
'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description'
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -154,13 +157,9 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
Q(description__icontains=value) |
|
||||
Q(physical_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)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
qs_filter |= Q(asns__asn=int(value.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
@ -217,7 +216,6 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
to_field_name='slug',
|
||||
label='Location (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
@ -233,18 +231,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
|
||||
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
|
||||
|
||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@ -317,7 +310,6 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
serial = django_filters.CharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
@ -338,11 +330,7 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
@ -381,7 +369,6 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
@ -399,18 +386,13 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
@ -445,11 +427,14 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
method='_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(
|
||||
method='_device_bays',
|
||||
label='Has device bays',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
@ -488,10 +473,85 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
rearporttemplates__isnull=value
|
||||
)
|
||||
|
||||
def _module_bays(self, queryset, name, value):
|
||||
return queryset.exclude(modulebaytemplates__isnull=value)
|
||||
|
||||
def _device_bays(self, queryset, name, 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):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@ -509,28 +569,36 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
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:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
feed_leg = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
null_value=None
|
||||
@ -541,7 +609,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompone
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceTypeChoices,
|
||||
null_value=None
|
||||
@ -552,7 +620,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
fields = ['id', 'name', 'type', 'mgmt_only']
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@ -563,7 +631,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
fields = ['id', 'name', 'type', 'color']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@ -574,6 +642,13 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
|
||||
fields = ['id', 'name', 'type', 'color', 'positions']
|
||||
|
||||
|
||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@ -581,8 +656,50 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
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):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@ -601,18 +718,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@ -763,11 +875,14 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
method='_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(
|
||||
method='_device_bays',
|
||||
label='Has device bays',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@ -814,10 +929,55 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
rearports__isnull=value
|
||||
)
|
||||
|
||||
def _module_bays(self, queryset, name, value):
|
||||
return queryset.exclude(modulebays__isnull=value)
|
||||
|
||||
def _device_bays(self, queryset, name, 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):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@ -892,7 +1052,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Virtual Chassis',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
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):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
@ -924,7 +1094,12 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
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(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@ -935,7 +1110,12 @@ class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class ConsoleServerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@ -946,7 +1126,12 @@ class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class PowerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypeChoices,
|
||||
null_value=None
|
||||
@ -957,7 +1142,12 @@ class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
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(
|
||||
choices=PowerOutletTypeChoices,
|
||||
null_value=None
|
||||
@ -972,11 +1162,12 @@ class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
|
||||
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class InterfaceFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
||||
# members
|
||||
device = MultiValueCharFilter(
|
||||
@ -1008,9 +1199,12 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
queryset=Interface.objects.all(),
|
||||
label='LAG interface (ID)',
|
||||
)
|
||||
speed = MultiValueNumberFilter()
|
||||
duplex = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceDuplexChoices
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter()
|
||||
wwn = MultiValueWWNFilter()
|
||||
tag = TagFilter()
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
label='Assigned VLAN'
|
||||
@ -1029,6 +1223,17 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
rf_channel = django_filters.MultipleChoiceFilter(
|
||||
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:
|
||||
model = Interface
|
||||
@ -1085,7 +1290,11 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
}.get(value, queryset.none())
|
||||
|
||||
|
||||
class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
class FrontPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@ -1096,7 +1305,11 @@ class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
fields = ['id', 'name', 'label', 'type', 'color', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
class RearPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@ -1107,18 +1320,21 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
|
||||
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:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
@ -1133,6 +1349,18 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
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()
|
||||
serial = django_filters.CharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
@ -1154,11 +1382,14 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
master_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Master (ID)',
|
||||
@ -1217,7 +1448,6 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
@ -1234,11 +1464,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
termination_a_type = ContentTypeFilter()
|
||||
termination_a_id = MultiValueNumberFilter()
|
||||
termination_b_type = ContentTypeFilter()
|
||||
@ -1275,7 +1501,6 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@ -1294,11 +1519,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@ -1341,7 +1562,6 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
lookup_expr='in',
|
||||
label='Location (ID)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
@ -1356,11 +1576,7 @@ class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region',
|
||||
@ -1411,7 +1627,6 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
choices=PowerFeedStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
|
@ -4,7 +4,7 @@ from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
|
||||
from .object_create import ComponentForm
|
||||
from .object_create import ComponentCreateForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortBulkCreateForm',
|
||||
@ -13,6 +13,7 @@ __all__ = (
|
||||
# 'FrontPortBulkCreateForm',
|
||||
'InterfaceBulkCreateForm',
|
||||
'InventoryItemBulkCreateForm',
|
||||
'ModuleBayBulkCreateForm',
|
||||
'PowerOutletBulkCreateForm',
|
||||
'PowerPortBulkCreateForm',
|
||||
'RearPortBulkCreateForm',
|
||||
@ -23,7 +24,7 @@ __all__ = (
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm):
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -71,12 +72,12 @@ class PowerOutletBulkCreateForm(
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
model = Interface
|
||||
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')
|
||||
|
||||
|
||||
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = ModuleBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = DeviceBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
model = InventoryItem
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
'tags',
|
||||
'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
@ -6,13 +6,12 @@ from timezone_field import TimeZoneFormField
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
|
||||
from ipam.models import VLAN, ASN
|
||||
from ipam.models import ASN, VLAN, VRF
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@ -31,8 +30,14 @@ __all__ = (
|
||||
'InterfaceBulkEditForm',
|
||||
'InterfaceTemplateBulkEditForm',
|
||||
'InventoryItemBulkEditForm',
|
||||
'InventoryItemRoleBulkEditForm',
|
||||
'InventoryItemTemplateBulkEditForm',
|
||||
'LocationBulkEditForm',
|
||||
'ManufacturerBulkEditForm',
|
||||
'ModuleBulkEditForm',
|
||||
'ModuleBayBulkEditForm',
|
||||
'ModuleBayTemplateBulkEditForm',
|
||||
'ModuleTypeBulkEditForm',
|
||||
'PlatformBulkEditForm',
|
||||
'PowerFeedBulkEditForm',
|
||||
'PowerOutletBulkEditForm',
|
||||
@ -52,11 +57,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
class RegionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
@ -66,15 +67,14 @@ class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = Region
|
||||
fieldsets = (
|
||||
(None, ('parent', 'description')),
|
||||
)
|
||||
nullable_fields = ('parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
@ -84,15 +84,14 @@ class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
(None, ('parent', 'description')),
|
||||
)
|
||||
nullable_fields = ('parent', 'description')
|
||||
|
||||
|
||||
class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(SiteStatusChoices),
|
||||
required=False,
|
||||
@ -111,12 +110,6 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
min_value=BGP_ASN_MIN,
|
||||
max_value=BGP_ASN_MAX,
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
@ -144,18 +137,16 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
widget=StaticSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'region', 'group', 'tenant', 'asn', 'asns', 'contact_name', 'contact_phone', 'contact_email', 'description',
|
||||
'time_zone',
|
||||
]
|
||||
|
||||
|
||||
class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = Site
|
||||
fieldsets = (
|
||||
(None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'region', 'group', 'tenant', 'asns', 'description', 'time_zone',
|
||||
)
|
||||
|
||||
|
||||
class LocationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
@ -176,15 +167,14 @@ class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['parent', 'tenant', 'description']
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = Location
|
||||
fieldsets = (
|
||||
(None, ('site', 'parent', 'tenant', 'description')),
|
||||
)
|
||||
nullable_fields = ('parent', 'tenant', 'description')
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
@ -193,15 +183,14 @@ class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = RackRole
|
||||
fieldsets = (
|
||||
(None, ('color', 'description')),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -291,17 +280,18 @@ class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
('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')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=User.objects.order_by(
|
||||
'username'
|
||||
@ -318,33 +308,33 @@ class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = RackReservation
|
||||
fieldsets = (
|
||||
(None, ('user', 'tenant', 'description')),
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = Manufacturer
|
||||
fieldsets = (
|
||||
(None, ('description',)),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
part_number = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False
|
||||
@ -360,15 +350,30 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
widget=StaticSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['airflow']
|
||||
|
||||
|
||||
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = DeviceType
|
||||
fieldsets = (
|
||||
(None, ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
|
||||
)
|
||||
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(
|
||||
required=False
|
||||
)
|
||||
@ -382,15 +387,14 @@ class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = DeviceRole
|
||||
fieldsets = (
|
||||
(None, ('color', 'vm_role', 'description')),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
@ -405,15 +409,14 @@ class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['manufacturer', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = Platform
|
||||
fieldsets = (
|
||||
(None, ('manufacturer', 'napalm_driver', 'description')),
|
||||
)
|
||||
nullable_fields = ('manufacturer', 'napalm_driver', 'description')
|
||||
|
||||
|
||||
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
@ -464,17 +467,43 @@ class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
label='Serial Number'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'tenant', 'platform', 'serial', 'airflow',
|
||||
]
|
||||
|
||||
|
||||
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Cable.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = Device
|
||||
fieldsets = (
|
||||
('Device', ('device_role', 'status', 'tenant', 'platform')),
|
||||
('Location', ('site', 'location')),
|
||||
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
|
||||
)
|
||||
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(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
required=False,
|
||||
@ -509,10 +538,14 @@ class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
widget=StaticSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||
]
|
||||
model = Cable
|
||||
fieldsets = (
|
||||
(None, ('type', 'status', 'tenant', 'label')),
|
||||
('Attributes', ('color', 'length', 'length_unit')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
@ -526,25 +559,20 @@ class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
||||
domain = forms.CharField(
|
||||
max_length=30,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['domain']
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = VirtualChassis
|
||||
fieldsets = (
|
||||
(None, ('domain',)),
|
||||
)
|
||||
nullable_fields = ('domain',)
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -575,15 +603,14 @@ class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['location']
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
model = PowerPanel
|
||||
fieldsets = (
|
||||
(None, ('region', 'site_group', 'site', 'location')),
|
||||
)
|
||||
nullable_fields = ('location',)
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
|
||||
power_panel = DynamicModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
required=False
|
||||
@ -634,10 +661,12 @@ class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'location', 'comments',
|
||||
]
|
||||
model = PowerFeed
|
||||
fieldsets = (
|
||||
(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()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
|
||||
@ -681,8 +709,7 @@ class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateBulkEditForm(BulkEditForm):
|
||||
@ -713,8 +740,7 @@ class PowerPortTemplateBulkEditForm(BulkEditForm):
|
||||
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):
|
||||
@ -750,8 +776,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm):
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -788,8 +813,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('label', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateBulkEditForm(BulkEditForm):
|
||||
@ -813,8 +837,7 @@ class FrontPortTemplateBulkEditForm(BulkEditForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class RearPortTemplateBulkEditForm(BulkEditForm):
|
||||
@ -838,8 +861,23 @@ class RearPortTemplateBulkEditForm(BulkEditForm):
|
||||
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):
|
||||
@ -855,90 +893,125 @@ class DeviceBayTemplateBulkEditForm(BulkEditForm):
|
||||
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
|
||||
#
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
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()
|
||||
)
|
||||
class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
disabled=True,
|
||||
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(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['label', 'type', 'feed_leg', 'power_port', 'description']
|
||||
model = ConsolePort
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -954,22 +1027,12 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power',
|
||||
]),
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
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(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
@ -987,7 +1050,13 @@ class InterfaceBulkEditForm(
|
||||
required=False,
|
||||
query_params={
|
||||
'type': 'lag',
|
||||
}
|
||||
},
|
||||
label='LAG'
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
widget=SelectSpeedWidget(),
|
||||
label='Speed'
|
||||
)
|
||||
mgmt_only = forms.NullBooleanField(
|
||||
required=False,
|
||||
@ -1006,12 +1075,26 @@ class InterfaceBulkEditForm(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
|
||||
]
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
|
||||
('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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -1075,59 +1158,83 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = FrontPort
|
||||
fieldsets = (
|
||||
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['label', 'description']
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
(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(
|
||||
form_from_model(DeviceBay, ['label', 'description']),
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
NetBoxModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
model = DeviceBay
|
||||
fieldsets = (
|
||||
(None, ('label', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['label', 'description']
|
||||
nullable_fields = ('label', 'description')
|
||||
|
||||
|
||||
class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
required=False
|
||||
)
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['label', 'manufacturer', 'part_id', 'description']
|
||||
model = InventoryItem
|
||||
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')
|
||||
|
@ -7,7 +7,8 @@ from django.utils.safestring import mark_safe
|
||||
from dcim.choices import *
|
||||
from dcim.constants 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 utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||
from virtualization.models import Cluster
|
||||
@ -24,8 +25,11 @@ __all__ = (
|
||||
'FrontPortCSVForm',
|
||||
'InterfaceCSVForm',
|
||||
'InventoryItemCSVForm',
|
||||
'InventoryItemRoleCSVForm',
|
||||
'LocationCSVForm',
|
||||
'ManufacturerCSVForm',
|
||||
'ModuleCSVForm',
|
||||
'ModuleBayCSVForm',
|
||||
'PlatformCSVForm',
|
||||
'PowerFeedCSVForm',
|
||||
'PowerOutletCSVForm',
|
||||
@ -42,7 +46,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionCSVForm(CustomFieldModelCSVForm):
|
||||
class RegionCSVForm(NetBoxModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -55,7 +59,7 @@ class RegionCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
||||
class SiteGroupCSVForm(NetBoxModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
@ -68,7 +72,7 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
class SiteCSVForm(NetBoxModelCSVForm):
|
||||
status = CSVChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
help_text='Operational status'
|
||||
@ -96,8 +100,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
model = Site
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
|
||||
)
|
||||
help_texts = {
|
||||
'time_zone': mark_safe(
|
||||
@ -106,7 +109,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
class LocationCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -133,7 +136,7 @@ class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description')
|
||||
|
||||
|
||||
class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
class RackRoleCSVForm(NetBoxModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -144,7 +147,7 @@ class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class RackCSVForm(CustomFieldModelCSVForm):
|
||||
class RackCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -202,7 +205,7 @@ class RackCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackReservationCSVForm(CustomFieldModelCSVForm):
|
||||
class RackReservationCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -252,14 +255,14 @@ class RackReservationCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
||||
class ManufacturerCSVForm(NetBoxModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ('name', 'slug', 'description')
|
||||
|
||||
|
||||
class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
||||
class DeviceRoleCSVForm(NetBoxModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -270,7 +273,7 @@ class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class PlatformCSVForm(CustomFieldModelCSVForm):
|
||||
class PlatformCSVForm(NetBoxModelCSVForm):
|
||||
slug = SlugField()
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@ -284,7 +287,7 @@ class PlatformCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
|
||||
|
||||
|
||||
class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||
class BaseDeviceCSVForm(NetBoxModelCSVForm):
|
||||
device_role = CSVModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -400,6 +403,35 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
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):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@ -446,7 +478,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
class ConsolePortCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -469,7 +501,7 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -492,7 +524,7 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
class PowerPortCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -510,7 +542,7 @@ class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
class PowerOutletCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -559,7 +591,7 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['power_port'].queryset = PowerPort.objects.none()
|
||||
|
||||
|
||||
class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
class InterfaceCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -586,11 +618,21 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
choices=InterfaceTypeChoices,
|
||||
help_text='Physical medium'
|
||||
)
|
||||
duplex = CSVChoiceField(
|
||||
choices=InterfaceDuplexChoices,
|
||||
required=False
|
||||
)
|
||||
mode = CSVChoiceField(
|
||||
choices=InterfaceModeChoices,
|
||||
required=False,
|
||||
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(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
@ -600,8 +642,8 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
|
||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'mac_address',
|
||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power',
|
||||
)
|
||||
|
||||
@ -626,7 +668,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
return self.cleaned_data['enabled']
|
||||
|
||||
|
||||
class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
class FrontPortCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -674,7 +716,7 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['rear_port'].queryset = RearPort.objects.none()
|
||||
|
||||
|
||||
class RearPortCSVForm(CustomFieldModelCSVForm):
|
||||
class RearPortCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
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(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
@ -738,11 +791,16 @@ class DeviceBayCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
||||
|
||||
|
||||
class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
class InventoryItemCSVForm(NetBoxModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
role = CSVModelChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -758,7 +816,8 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
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):
|
||||
@ -777,7 +836,26 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
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
|
||||
side_a_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@ -878,7 +956,11 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
return length_unit if length_unit is not None else ''
|
||||
|
||||
|
||||
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisCSVForm(NetBoxModelCSVForm):
|
||||
master = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -891,7 +973,11 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
fields = ('name', 'domain', 'master')
|
||||
|
||||
|
||||
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
#
|
||||
# Power
|
||||
#
|
||||
|
||||
class PowerPanelCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -917,7 +1003,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||
class PowerFeedCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
|
@ -1,7 +1,7 @@
|
||||
from circuits.models import Circuit, CircuitTermination, Provider
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
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
|
||||
"""
|
||||
@ -70,10 +70,6 @@ class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
|
||||
'rack_id': '$termination_b_rack',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@ -171,7 +167,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
|
||||
class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
|
||||
termination_b_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
@ -212,10 +208,6 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
|
||||
'circuit_id': '$termination_b_circuit'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
@ -229,7 +221,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
|
||||
class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
@ -274,10 +266,6 @@ class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
|
||||
'power_panel_id': '$termination_b_powerpanel'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
|
@ -5,13 +5,13 @@ from django.utils.translation import gettext as _
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from tenancy.models import *
|
||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||
from ipam.models import ASN
|
||||
from extras.forms import LocalConfigContextFilterForm
|
||||
from ipam.models import ASN, VRF
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from utilities.forms import (
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField,
|
||||
StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
|
||||
)
|
||||
from wireless.choices import *
|
||||
|
||||
@ -28,8 +28,13 @@ __all__ = (
|
||||
'InterfaceConnectionFilterForm',
|
||||
'InterfaceFilterForm',
|
||||
'InventoryItemFilterForm',
|
||||
'InventoryItemRoleFilterForm',
|
||||
'LocationFilterForm',
|
||||
'ManufacturerFilterForm',
|
||||
'ModuleFilterForm',
|
||||
'ModuleFilterForm',
|
||||
'ModuleBayFilterForm',
|
||||
'ModuleTypeFilterForm',
|
||||
'PlatformFilterForm',
|
||||
'PowerConnectionFilterForm',
|
||||
'PowerFeedFilterForm',
|
||||
@ -48,7 +53,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(CustomFieldModelFilterForm):
|
||||
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -99,13 +104,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Region
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['parent_id'],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -114,13 +118,12 @@ class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = SiteGroup
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['parent_id'],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
@ -129,19 +132,17 @@ class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Site
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['status', 'region_id', 'group_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['asn_id'],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
status = forms.MultipleChoiceField(
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
required=False
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@ -161,14 +162,14 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModel
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Location
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'parent_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -200,21 +201,21 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldM
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackRoleFilterForm(CustomFieldModelFilterForm):
|
||||
class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
model = RackRole
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Rack
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['status', 'role_id'],
|
||||
['type', 'width', 'serial', 'asset_tag'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['contact', 'contact_role']
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_id', 'location_id')),
|
||||
('Function', ('status', 'role_id')),
|
||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -237,20 +238,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModel
|
||||
},
|
||||
label=_('Location')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=RackTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
width = forms.MultipleChoiceField(
|
||||
width = MultipleChoiceField(
|
||||
choices=RackWidthChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
@ -279,14 +277,14 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = RackReservation
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['user_id'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('User', ('user_id',)),
|
||||
('Rack', ('region_id', 'site_id', 'location_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -317,36 +315,40 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Manufacturer
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(CustomFieldModelFilterForm):
|
||||
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = DeviceType
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['manufacturer_id', 'subdevice_role', 'airflow'],
|
||||
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
||||
('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')
|
||||
)
|
||||
subdevice_role = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(SubdeviceRoleChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
part_number = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
subdevice_role = MultipleChoiceField(
|
||||
choices=add_blank_choice(SubdeviceRoleChoices),
|
||||
required=False
|
||||
)
|
||||
airflow = MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
@ -393,12 +395,76 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
|
||||
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
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PlatformFilterForm(CustomFieldModelFilterForm):
|
||||
class PlatformFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Platform
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@ -408,20 +474,25 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class DeviceFilterForm(
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
ContactModelFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = Device
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'],
|
||||
['status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address'],
|
||||
['manufacturer_id', 'device_type_id', 'platform_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
[
|
||||
'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports',
|
||||
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
|
||||
],
|
||||
['contact', 'contact_role'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
||||
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
('Components', (
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||
)),
|
||||
('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'local_context_data'))
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -484,15 +555,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactM
|
||||
null_option='None',
|
||||
label=_('Platform')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
airflow = MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@ -563,13 +632,43 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactM
|
||||
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
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -592,14 +691,14 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Cable
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['site_id', 'rack_id', 'device_id'],
|
||||
['type', 'status', 'color', 'length', 'length_unit'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('site_id', 'rack_id', 'device_id')),
|
||||
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -632,15 +731,13 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(LinkStatusChoices),
|
||||
widget=StaticSelect()
|
||||
choices=add_blank_choice(LinkStatusChoices)
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
@ -655,12 +752,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = PowerPanel
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('region_id', 'site_group_id', 'site_id', 'location_id'),
|
||||
('contact', 'contact_role')
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@ -693,14 +790,13 @@ class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerFeedFilterForm(CustomFieldModelFilterForm):
|
||||
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||
model = PowerFeed
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['power_panel_id', 'rack_id'],
|
||||
['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
|
||||
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -737,10 +833,9 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
|
||||
},
|
||||
label=_('Rack')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerFeedTypeChoices),
|
||||
@ -775,91 +870,93 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
|
||||
|
||||
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
speed = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
speed = MultipleChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
speed = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
speed = MultipleChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
kind = forms.MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||
('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')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
kind = MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
label='Select Speed',
|
||||
widget=SelectSpeedWidget(attrs={'readonly': None})
|
||||
)
|
||||
duplex = MultipleChoiceField(
|
||||
choices=InterfaceDuplexChoices,
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
@ -881,16 +978,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
rf_role = forms.MultipleChoiceField(
|
||||
rf_role = MultipleChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless role'
|
||||
)
|
||||
rf_channel = forms.MultipleChoiceField(
|
||||
rf_channel = MultipleChoiceField(
|
||||
choices=WirelessChannelChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless channel'
|
||||
)
|
||||
rf_channel_frequency = forms.IntegerField(
|
||||
@ -907,20 +1002,24 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
min_value=0,
|
||||
max_value=127
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'color')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
@ -930,15 +1029,14 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
|
||||
class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'color')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
@ -946,23 +1044,42 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
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):
|
||||
model = DeviceBay
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
model = InventoryItem
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||
('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(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@ -983,6 +1100,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Device component roles
|
||||
#
|
||||
|
||||
class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
model = InventoryItemRole
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,41 +1,25 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
)
|
||||
from wireless.choices import *
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortCreateForm',
|
||||
'ConsolePortTemplateCreateForm',
|
||||
'ConsoleServerPortCreateForm',
|
||||
'ConsoleServerPortTemplateCreateForm',
|
||||
'DeviceBayCreateForm',
|
||||
'DeviceBayTemplateCreateForm',
|
||||
'ComponentTemplateCreateForm',
|
||||
'DeviceComponentCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InterfaceCreateForm',
|
||||
'InterfaceTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
'PowerPortCreateForm',
|
||||
'PowerPortTemplateCreateForm',
|
||||
'RearPortCreateForm',
|
||||
'RearPortTemplateCreateForm',
|
||||
'ModularComponentTemplateCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
'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
|
||||
a name pattern.
|
||||
@ -52,18 +36,170 @@ class ComponentForm(BootstrapMixin, forms.Form):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of components being created from both the name_pattern and label_pattern are equal
|
||||
if self.cleaned_data['label_pattern']:
|
||||
name_pattern_count = len(self.cleaned_data['name_pattern'])
|
||||
label_pattern_count = len(self.cleaned_data['label_pattern'])
|
||||
if name_pattern_count != label_pattern_count:
|
||||
# Validate that all patterned fields generate an equal number of values
|
||||
patterned_fields = [
|
||||
field_name for field_name in self.fields if field_name.endswith('_pattern')
|
||||
]
|
||||
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({
|
||||
'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however '
|
||||
f'{label_pattern_count} labels will be generated. These counts must match.'
|
||||
field_name: f'The provided pattern specifies {value_count} values, but {pattern_count} are '
|
||||
f'expected.'
|
||||
}, 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(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -107,10 +243,6 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
required=False,
|
||||
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:
|
||||
model = VirtualChassis
|
||||
@ -136,521 +268,3 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
member.save()
|
||||
|
||||
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',
|
||||
)
|
||||
|
@ -11,6 +11,9 @@ __all__ = (
|
||||
'DeviceTypeImportForm',
|
||||
'FrontPortTemplateImportForm',
|
||||
'InterfaceTemplateImportForm',
|
||||
'InventoryItemTemplateImportForm',
|
||||
'ModuleBayTemplateImportForm',
|
||||
'ModuleTypeImportForm',
|
||||
'PowerOutletTemplateImportForm',
|
||||
'PowerPortTemplateImportForm',
|
||||
'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
|
||||
#
|
||||
|
||||
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
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
|
||||
pass
|
||||
|
||||
|
||||
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
@ -63,7 +58,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -72,7 +67,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@ -81,7 +76,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
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:
|
||||
model = PowerOutletTemplate
|
||||
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):
|
||||
type = forms.ChoiceField(
|
||||
@ -107,7 +116,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
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'
|
||||
)
|
||||
|
||||
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:
|
||||
model = FrontPortTemplate
|
||||
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:
|
||||
model = RearPortTemplate
|
||||
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 = [
|
||||
'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
|
||||
|
@ -50,12 +50,30 @@ class DCIMQuery(graphene.ObjectType):
|
||||
inventory_item = ObjectField(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_list = ObjectListField(LocationType)
|
||||
|
||||
manufacturer = ObjectField(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_list = ObjectListField(PlatformType)
|
||||
|
||||
|
@ -6,7 +6,7 @@ from extras.graphql.mixins import (
|
||||
)
|
||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||
from netbox.graphql.scalars import BigInt
|
||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
|
||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
|
||||
__all__ = (
|
||||
'CableType',
|
||||
@ -25,8 +25,14 @@ __all__ = (
|
||||
'InterfaceType',
|
||||
'InterfaceTemplateType',
|
||||
'InventoryItemType',
|
||||
'InventoryItemRoleType',
|
||||
'InventoryItemTemplateType',
|
||||
'LocationType',
|
||||
'ManufacturerType',
|
||||
'ModuleType',
|
||||
'ModuleBayType',
|
||||
'ModuleBayTemplateType',
|
||||
'ModuleTypeType',
|
||||
'PlatformType',
|
||||
'PowerFeedType',
|
||||
'PowerOutletType',
|
||||
@ -79,7 +85,7 @@ class ComponentTemplateObjectType(
|
||||
# Model types
|
||||
#
|
||||
|
||||
class CableType(PrimaryObjectType):
|
||||
class CableType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
@ -137,7 +143,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
@ -167,6 +173,14 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
|
||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||
|
||||
|
||||
class InventoryItemTemplateType(ComponentTemplateObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceRoleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
@ -175,7 +189,7 @@ class DeviceRoleType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.DeviceRoleFilterSet
|
||||
|
||||
|
||||
class DeviceTypeType(PrimaryObjectType):
|
||||
class DeviceTypeType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
@ -238,6 +252,14 @@ class InventoryItemType(ComponentObjectType):
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
|
||||
|
||||
class InventoryItemRoleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemRole
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||
|
||||
|
||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
@ -254,6 +276,38 @@ class ManufacturerType(OrganizationalObjectType):
|
||||
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 Meta:
|
||||
@ -262,7 +316,7 @@ class PlatformType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.PlatformFilterSet
|
||||
|
||||
|
||||
class PowerFeedType(PrimaryObjectType):
|
||||
class PowerFeedType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
@ -298,7 +352,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class PowerPanelType(PrimaryObjectType):
|
||||
class PowerPanelType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
@ -328,7 +382,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
@ -342,7 +396,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
||||
return self.outer_unit or None
|
||||
|
||||
|
||||
class RackReservationType(PrimaryObjectType):
|
||||
class RackReservationType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
@ -382,7 +436,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
|
||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
asn = graphene.Field(BigInt)
|
||||
|
||||
class Meta:
|
||||
@ -399,7 +453,7 @@ class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
|
||||
class VirtualChassisType(PrimaryObjectType):
|
||||
class VirtualChassisType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user