mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 02:06:42 -06:00
Merge feature
This commit is contained in:
commit
75aa1c7b80
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.4
|
placeholder: v3.1.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.4
|
placeholder: v3.1.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -82,6 +82,10 @@ markdown-include
|
|||||||
# https://github.com/squidfunk/mkdocs-material
|
# https://github.com/squidfunk/mkdocs-material
|
||||||
mkdocs-material
|
mkdocs-material
|
||||||
|
|
||||||
|
# Introspection for embedded code
|
||||||
|
# https://github.com/mkdocstrings/mkdocstrings
|
||||||
|
mkdocstrings
|
||||||
|
|
||||||
# Library for manipulating IP prefixes and addresses
|
# Library for manipulating IP prefixes and addresses
|
||||||
# https://github.com/drkjam/netaddr
|
# https://github.com/drkjam/netaddr
|
||||||
netaddr
|
netaddr
|
||||||
@ -98,10 +102,6 @@ psycopg2-binary
|
|||||||
# https://github.com/yaml/pyyaml
|
# https://github.com/yaml/pyyaml
|
||||||
PyYAML
|
PyYAML
|
||||||
|
|
||||||
# In-memory key/value store used for caching and queuing
|
|
||||||
# https://github.com/andymccurdy/redis-py
|
|
||||||
redis
|
|
||||||
|
|
||||||
# Social authentication framework
|
# Social authentication framework
|
||||||
# https://github.com/python-social-auth/social-core
|
# https://github.com/python-social-auth/social-core
|
||||||
social-auth-core[all]
|
social-auth-core[all]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
# Service Mapping
|
# Service Mapping
|
||||||
|
|
||||||
|
{!models/ipam/servicetemplate.md!}
|
||||||
{!models/ipam/service.md!}
|
{!models/ipam/service.md!}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. Define the model class
|
## 1. Define the model class
|
||||||
|
|
||||||
Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be PrimaryModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module.
|
Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be NetBoxModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module.
|
||||||
|
|
||||||
Each model should define, at a minimum:
|
Each model should define, at a minimum:
|
||||||
|
|
||||||
|
@ -114,6 +114,12 @@ This ensures that your development environment is now complete and operational.
|
|||||||
!!! info "IDE Integration"
|
!!! info "IDE Integration"
|
||||||
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||||
|
|
||||||
|
## Populating Demo Data
|
||||||
|
|
||||||
|
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
|
||||||
|
|
||||||
|
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command.
|
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.
|
||||||
|
@ -5,8 +5,10 @@ The `users.UserConfig` model holds individual preferences for each user in the f
|
|||||||
## Available Preferences
|
## Available Preferences
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|-------------------------|-------------|
|
|--------------------------|---------------------------------------------------------------|
|
||||||
| data_format | Preferred format when rendering raw data (JSON or YAML) |
|
| data_format | Preferred format when rendering raw data (JSON or YAML) |
|
||||||
| pagination.per_page | The number of items to display per page of a paginated table |
|
| pagination.per_page | The number of items to display per page of a paginated table |
|
||||||
|
| 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}.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 |
|
| ui.colormode | Light or dark mode in the user interface |
|
||||||
|
@ -50,7 +50,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
|||||||
| Application | Django/Python |
|
| Application | Django/Python |
|
||||||
| Database | PostgreSQL 10+ |
|
| Database | PostgreSQL 10+ |
|
||||||
| Task queuing | Redis/django-rq |
|
| Task queuing | Redis/django-rq |
|
||||||
| Live device access | NAPALM |
|
| Live device access | NAPALM (optional) |
|
||||||
|
|
||||||
## Supported Python Versions
|
## Supported Python Versions
|
||||||
|
|
||||||
@ -58,4 +58,6 @@ NetBox supports Python 3.8, 3.9, and 3.10 environments.
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
See the [installation guide](installation/index.md) for help getting NetBox up and running quickly.
|
Minor NetBox releases (e.g. v3.1) are published three times a year; in April, August, and December. These typically introduce major new features and may contain breaking API changes. Patch releases are published roughly every one to two weeks to resolve bugs and fulfill minor feature requests. These are backward-compatible with previous releases unless otherwise noted. The NetBox maintainers strongly recommend running the latest stable release whenever possible.
|
||||||
|
|
||||||
|
Please see the [official installation guide](installation/index.md) for detailed instructions on obtaining and installing NetBox.
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB |
@ -1,6 +1,6 @@
|
|||||||
## Interfaces
|
## Interfaces
|
||||||
|
|
||||||
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
|
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Additionally, each interface may optionally be assigned to a VRF.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
|
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
|
||||||
|
@ -19,6 +19,8 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
|
|||||||
* JSON: Arbitrary data stored in JSON format
|
* JSON: Arbitrary data stored in JSON format
|
||||||
* Selection: A selection of one of several pre-defined custom choices
|
* Selection: A selection of one of several pre-defined custom choices
|
||||||
* Multiple selection: A selection field which supports the assignment of multiple values
|
* Multiple selection: A selection field which supports the assignment of multiple values
|
||||||
|
* Object: A single NetBox object of the type defined by `object_type`
|
||||||
|
* Multiple object: One or more NetBox objects of the type defined by `object_type`
|
||||||
|
|
||||||
Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||||
|
|
||||||
@ -41,3 +43,7 @@ NetBox supports limited custom validation for custom field values. Following are
|
|||||||
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
|
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
|
||||||
|
|
||||||
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
|
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
|
||||||
|
|
||||||
|
### Custom Object Fields
|
||||||
|
|
||||||
|
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.
|
||||||
|
@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as:
|
|||||||
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
|
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
|
Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
|
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
|
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
|
Webhooks support the inclusion of user-submitted code to generate URL, custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ A webhook is a mechanism for conveying to some external system a change that too
|
|||||||
* **Enabled** - If unchecked, the webhook will be inactive.
|
* **Enabled** - If unchecked, the webhook will be inactive.
|
||||||
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
|
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
|
||||||
* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
|
* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
|
||||||
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed.
|
* **URL** - The fully-qualified URL of the request to be sent. This may specify a destination port number if needed. Jinja2 templating is supported for this field.
|
||||||
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
|
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
|
||||||
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
||||||
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
||||||
@ -23,7 +23,7 @@ A webhook is a mechanism for conveying to some external system a change that too
|
|||||||
|
|
||||||
## Jinja2 Template Support
|
## Jinja2 Template Support
|
||||||
|
|
||||||
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
|
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
|
||||||
|
|
||||||
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
|
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
|
||||||
|
|
||||||
|
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.
|
@ -1,430 +0,0 @@
|
|||||||
# Plugin Development
|
|
||||||
|
|
||||||
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.8/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`) |
|
|
||||||
| `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 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.8/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
|
|
||||||
```
|
|
27
docs/plugins/development/background-tasks.md
Normal file
27
docs/plugins/development/background-tasks.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
|
```
|
56
docs/plugins/development/filtersets.md
Normal file
56
docs/plugins/development/filtersets.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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 the subclass of a generic view, such as `ObjectListView` or `BulkEditView`, set it as 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 on a REST API endpoint, set it as 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
|
||||||
|
```
|
146
docs/plugins/development/index.md
Normal file
146
docs/plugins/development/index.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Plugins 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.8/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`) |
|
||||||
|
| `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 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.8/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
|
||||||
|
```
|
111
docs/plugins/development/models.md
Normal file
111
docs/plugins/development/models.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# 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. 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
|
||||||
|
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/).
|
||||||
|
|
||||||
|
## 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 numerous feature, 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, or attributes necessary to the operation of these features
|
||||||
|
2. Register the model with NetBox as utilizing these feature
|
||||||
|
|
||||||
|
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. (You will also need to inherit from Django's built-in `Model` class.)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# models.py
|
||||||
|
from django.db import models
|
||||||
|
from netbox.models.features import ExportTemplatesMixin, TagsMixin
|
||||||
|
|
||||||
|
class MyModel(ExportTemplatesMixin, TagsMixin, models.Model):
|
||||||
|
foo = models.CharField()
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above will enable export templates and tags, but no other NetBox features. A complete list of available feature mixins is included below. (Inheriting all the available mixins is essentially the same as subclassing `BaseModel`.)
|
||||||
|
|
||||||
|
## Feature Mixins Reference
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
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
|
46
docs/plugins/development/rest-api.md
Normal file
46
docs/plugins/development/rest-api.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 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. 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.
|
37
docs/plugins/development/tables.md
Normal file
37
docs/plugins/development/tables.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# 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
|
||||||
|
* `id` - The object's numeric database ID, as a hyperlink to the object's view
|
||||||
|
* `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', ...)
|
||||||
|
```
|
254
docs/plugins/development/views.md
Normal file
254
docs/plugins/development/views.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
### Plugin Views
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Object Views Reference
|
||||||
|
|
||||||
|
Below is the class definition for NetBox's BaseObjectView. The attributes and methods defined here are available on all generic views which handle a single object.
|
||||||
|
|
||||||
|
::: netbox.views.generic.base.BaseObjectView
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
- get_template_name
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectEditView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
- alter_object
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectDeleteView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
## Multi-Object Views Reference
|
||||||
|
|
||||||
|
Below is the class definition for NetBox's BaseMultiObjectView. The attributes and methods defined here are available on all generic views which deal with multiple objects.
|
||||||
|
|
||||||
|
::: netbox.views.generic.base.BaseMultiObjectView
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectListView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_table
|
||||||
|
- export_table
|
||||||
|
- export_template
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkImportView
|
||||||
|
selection:
|
||||||
|
members: false
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkEditView
|
||||||
|
selection:
|
||||||
|
members: false
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkDeleteView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_form
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
@ -1,6 +1,14 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
Listed below are the major features introduced in each NetBox release. For more detail on a specific release train, see its individual release notes page.
|
NetBox releases are numbered as major, minor, and patch releases. For example, version 3.1.0 is a minor release, and v3.1.5 is a patch release. Briefly, these can be described as follows:
|
||||||
|
|
||||||
|
* **Major** - Introduces or removes an entire API or other core functionality
|
||||||
|
* **Minor** - Implements major new features but may include breaking changes for API consumers or other integrations
|
||||||
|
* **Patch** - A maintenance release which fixes bugs and may introduce backward-compatible enhancements
|
||||||
|
|
||||||
|
Minor releases are published in April, August, and December of each calendar year. Patch releases are published as needed to address bugs and fulfill minor feature requests, typically around every one to two weeks.
|
||||||
|
|
||||||
|
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.1](./version-3.1.md) (December 2021)
|
#### [Version 3.1](./version-3.1.md) (December 2021)
|
||||||
|
|
||||||
|
@ -1,6 +1,52 @@
|
|||||||
# NetBox v3.1
|
# NetBox v3.1
|
||||||
|
|
||||||
## v3.1.5 (FUTURE)
|
## v3.1.7 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.1.6 (2022-01-17)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8246](https://github.com/netbox-community/netbox/issues/8246) - Show human-friendly values for commit rates in circuits table
|
||||||
|
* [#8262](https://github.com/netbox-community/netbox/issues/8262) - Add cable count to tenant stats
|
||||||
|
* [#8265](https://github.com/netbox-community/netbox/issues/8265) - Add Stackwise-n interface types
|
||||||
|
* [#8293](https://github.com/netbox-community/netbox/issues/8293) - Show 4-byte ASNs in ASDOT notation
|
||||||
|
* [#8302](https://github.com/netbox-community/netbox/issues/8302) - Linkify role column in device & VM tables
|
||||||
|
* [#8337](https://github.com/netbox-community/netbox/issues/8337) - Enable sorting object tables by created & updated times
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#8279](https://github.com/netbox-community/netbox/issues/8279) - Fix display of virtual chassis members in rack elevations
|
||||||
|
* [#8285](https://github.com/netbox-community/netbox/issues/8285) - Fix `cluster_count` under tenant REST API serializer
|
||||||
|
* [#8287](https://github.com/netbox-community/netbox/issues/8287) - Correct label in export template form
|
||||||
|
* [#8301](https://github.com/netbox-community/netbox/issues/8301) - Fix delete button for various object children views
|
||||||
|
* [#8305](https://github.com/netbox-community/netbox/issues/8305) - Fix assignment of custom field data to FHRP groups via UI
|
||||||
|
* [#8306](https://github.com/netbox-community/netbox/issues/8306) - Redirect user to previous page after login
|
||||||
|
* [#8314](https://github.com/netbox-community/netbox/issues/8314) - Prevent custom fields with default values from appearing as applied filters erroneously
|
||||||
|
* [#8317](https://github.com/netbox-community/netbox/issues/8317) - Fix CSV import of multi-select custom field values
|
||||||
|
* [#8319](https://github.com/netbox-community/netbox/issues/8319) - Custom URL fields should honor `ALLOWED_URL_SCHEMES` config parameter
|
||||||
|
* [#8342](https://github.com/netbox-community/netbox/issues/8342) - Restore `created` & `last_updated` fields missing from several REST API serializers
|
||||||
|
* [#8357](https://github.com/netbox-community/netbox/issues/8357) - Add missing tags field to location filter form
|
||||||
|
* [#8358](https://github.com/netbox-community/netbox/issues/8358) - Fix inconsistent styling of custom fields on filter & bulk edit forms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.1.5 (2022-01-06)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
|
||||||
|
* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
|
||||||
|
* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
|
||||||
|
* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
|
||||||
|
* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
|
||||||
|
* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
|
||||||
|
* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -14,13 +14,15 @@
|
|||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
#### Automatic Provisioning of Next Available VLANs ([#2658](https://github.com/netbox-community/netbox/issues/2658))
|
#### Plugins Framework Extensions ([#8333](https://github.com/netbox-community/netbox/issues/8333))
|
||||||
|
|
||||||
A new REST API endpoint has been added at `/api/ipam/vlan-groups/<pk>/available-vlans/`. A GET request to this endpoint will return a list of available VLANs within the group. A POST request can be made to this endpoint specifying the name(s) of one or more VLANs to create within the group, and their VLAN IDs will be assigned automatically.
|
NetBox's plugins framework has been extended considerably in this release. Changes include:
|
||||||
|
|
||||||
#### Inventory Item Roles ([#3087](https://github.com/netbox-community/netbox/issues/3087))
|
* Seven generic view classes are now officially supported for use by plugins.
|
||||||
|
* `NetBoxModel` is available for subclassing to enable various NetBox features, such as custom fields and change logging.
|
||||||
|
* `NetBoxModelFilterSet` is available to extend NetBox's dynamic filtering ability to plugin models.
|
||||||
|
|
||||||
A new model has been introduced to represent function roles for inventory items, similar to device roles. The assignment of roles to inventory items is optional.
|
No breaking changes to previously supported components have been introduced in this release. However, plugin authors are encouraged to audit their 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))
|
#### Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
|
||||||
|
|
||||||
@ -28,6 +30,12 @@ Several new models have been added to support field-replaceable device modules,
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
#### Custom Object Fields ([#7006](https://github.com/netbox-community/netbox/issues/7006))
|
||||||
|
|
||||||
|
Two new types of custom field have been added: object and multi-object. These can be used to associate objects with other objects in NetBox. For example, you might create a custom field named `primary_site` on the tenant model so that a particular site can be associated with each tenant as its primary. The multi-object custom field type allows for the assignment of one or more objects of the same type.
|
||||||
|
|
||||||
|
Custom field object assignment is fully supported in the REST API, and functions similarly to normal foreign key relations. Nested representations are provided for each custom field object.
|
||||||
|
|
||||||
#### Custom Status Choices ([#8054](https://github.com/netbox-community/netbox/issues/8054))
|
#### Custom Status Choices ([#8054](https://github.com/netbox-community/netbox/issues/8054))
|
||||||
|
|
||||||
Custom choices can be now added to most status fields in NetBox. This is done by defining the `FIELD_CHOICES` configuration parameter to map field identifiers to an iterable of custom choices. These choices are populated automatically when NetBox initializes. For example, the following will add three custom choices for the site status field:
|
Custom choices can be now added to most status fields in NetBox. This is done by defining the `FIELD_CHOICES` configuration parameter to map field identifiers to an iterable of custom choices. These choices are populated automatically when NetBox initializes. For example, the following will add three custom choices for the site status field:
|
||||||
@ -42,20 +50,40 @@ FIELD_CHOICES = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Inventory Item Roles ([#3087](https://github.com/netbox-community/netbox/issues/3087))
|
||||||
|
|
||||||
|
A new model has been introduced to represent function 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 Item Templates ([#8118](https://github.com/netbox-community/netbox/issues/8118))
|
||||||
|
|
||||||
Inventory items can now be templatized on a device type similar to the other component types. This enables users to better pre-model fixed hardware components.
|
Inventory items can now be templatized on a device type similar to the other component types. This enables users to better pre-model fixed hardware components.
|
||||||
|
|
||||||
Inventory item templates can be arranged hierarchically within a device type, and may be assigned to other components. These relationships will be mirrored when instantiating inventory items on a newly-created device.
|
Inventory item templates can be arranged hierarchically within a device type, and may be assigned to other components. These relationships will be mirrored when instantiating inventory items on a newly-created device.
|
||||||
|
|
||||||
|
#### 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 application of layer four services to 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/<pk>/available-vlans/`. A GET request to this endpoint will return a list of available VLANs within the group. A POST request can be made to this endpoint specifying the name(s) of one or more VLANs to create within the group, and their VLAN IDs will be assigned automatically.
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5429](https://github.com/netbox-community/netbox/issues/5429) - Enable toggling the placement of table paginators
|
||||||
|
* [#6954](https://github.com/netbox-community/netbox/issues/6954) - Remember users' table ordering preferences
|
||||||
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
|
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
|
||||||
|
* [#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
|
* [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks
|
||||||
* [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form
|
* [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form
|
||||||
* [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts
|
* [#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
|
* [#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 assigning interfaces to VRFs
|
||||||
|
* [#7853](https://github.com/netbox-community/netbox/issues/7853) - Add `speed` and `duplex` fields to interface model
|
||||||
* [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
|
* [#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) - Webhook URLs can now be templatized
|
||||||
|
* [#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
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
@ -63,6 +91,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
|
|||||||
* [#7743](https://github.com/netbox-community/netbox/issues/7743) - Remove legacy ASN field from site model
|
* [#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
|
* [#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
|
* [#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
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
@ -73,6 +102,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
|
|||||||
* `/api/dcim/module-bays/`
|
* `/api/dcim/module-bays/`
|
||||||
* `/api/dcim/module-bay-templates/`
|
* `/api/dcim/module-bay-templates/`
|
||||||
* `/api/dcim/module-types/`
|
* `/api/dcim/module-types/`
|
||||||
|
* `/api/extras/service-templates/`
|
||||||
* circuits.ProviderNetwork
|
* circuits.ProviderNetwork
|
||||||
* Added `service_id` field
|
* Added `service_id` field
|
||||||
* dcim.ConsolePort
|
* dcim.ConsolePort
|
||||||
@ -82,7 +112,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
|
|||||||
* dcim.FrontPort
|
* dcim.FrontPort
|
||||||
* Added `module` field
|
* Added `module` field
|
||||||
* dcim.Interface
|
* dcim.Interface
|
||||||
* Added `module` field
|
* Added `module`, `speed`, `duplex`, and `vrf` fields
|
||||||
* dcim.InventoryItem
|
* dcim.InventoryItem
|
||||||
* Added `component_type`, `component_id`, and `role` fields
|
* Added `component_type`, `component_id`, and `role` fields
|
||||||
* Added read-only `component` field
|
* Added read-only `component` field
|
||||||
@ -96,6 +126,10 @@ Inventory item templates can be arranged hierarchically within a device type, an
|
|||||||
* Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
|
* Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
|
||||||
* extras.ConfigContext
|
* extras.ConfigContext
|
||||||
* Add `cluster_types` field
|
* Add `cluster_types` field
|
||||||
|
* extras.CustomField
|
||||||
|
* Added `object_type` field
|
||||||
|
* extras.CustomLink
|
||||||
|
* Added `enabled` field
|
||||||
* ipam.VLANGroup
|
* ipam.VLANGroup
|
||||||
* Added the `/availables-vlans/` endpoint
|
* Added the `/availables-vlans/` endpoint
|
||||||
* Added the `min_vid` and `max_vid` fields
|
* Added the `min_vid` and `max_vid` fields
|
||||||
|
@ -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
|
|
25
mkdocs.yml
25
mkdocs.yml
@ -16,6 +16,22 @@ theme:
|
|||||||
toggle:
|
toggle:
|
||||||
icon: material/lightbulb
|
icon: material/lightbulb
|
||||||
name: Switch to Light Mode
|
name: Switch to Light Mode
|
||||||
|
plugins:
|
||||||
|
- mkdocstrings:
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
setup_commands:
|
||||||
|
- import os
|
||||||
|
- import django
|
||||||
|
- os.chdir('netbox/')
|
||||||
|
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||||
|
- django.setup()
|
||||||
|
rendering:
|
||||||
|
heading_level: 3
|
||||||
|
members_order: source
|
||||||
|
show_root_heading: true
|
||||||
|
show_root_full_path: false
|
||||||
|
show_root_toc_entry: false
|
||||||
extra:
|
extra:
|
||||||
social:
|
social:
|
||||||
- icon: fontawesome/brands/github
|
- icon: fontawesome/brands/github
|
||||||
@ -84,7 +100,14 @@ nav:
|
|||||||
- Webhooks: 'additional-features/webhooks.md'
|
- Webhooks: 'additional-features/webhooks.md'
|
||||||
- Plugins:
|
- Plugins:
|
||||||
- Using Plugins: 'plugins/index.md'
|
- Using Plugins: 'plugins/index.md'
|
||||||
- Developing Plugins: 'plugins/development.md'
|
- Developing Plugins:
|
||||||
|
- Getting Started: 'plugins/development/index.md'
|
||||||
|
- Models: 'plugins/development/models.md'
|
||||||
|
- Views: 'plugins/development/views.md'
|
||||||
|
- Tables: 'plugins/development/tables.md'
|
||||||
|
- Filter Sets: 'plugins/development/filtersets.md'
|
||||||
|
- REST API: 'plugins/development/rest-api.md'
|
||||||
|
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||||
- Administration:
|
- Administration:
|
||||||
- Authentication: 'administration/authentication.md'
|
- Authentication: 'administration/authentication.md'
|
||||||
- Permissions: 'administration/permissions.md'
|
- Permissions: 'administration/permissions.md'
|
||||||
|
@ -100,5 +100,5 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||||
'_occupied',
|
'_occupied', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
@ -3,8 +3,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from dcim.filtersets import CableTerminationFilterSet
|
from dcim.filtersets import CableTerminationFilterSet
|
||||||
from dcim.models import Region, Site, SiteGroup
|
from dcim.models import Region, Site, SiteGroup
|
||||||
from extras.filters import TagFilter
|
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -19,7 +18,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProviderFilterSet(PrimaryModelFilterSet):
|
class ProviderFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -61,7 +60,6 @@ class ProviderFilterSet(PrimaryModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
@ -79,7 +77,7 @@ class ProviderFilterSet(PrimaryModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -94,7 +92,6 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Provider (slug)',
|
label='Provider (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
@ -112,14 +109,13 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -190,7 +186,6 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
@ -5,8 +5,8 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from dcim.models import LinkTermination
|
from dcim.models import LinkTermination
|
||||||
from extras.utils import extras_features
|
from netbox.models import ChangeLoggedModel, OrganizationalModel, NetBoxModel
|
||||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
from netbox.models.features import WebhooksMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Circuit',
|
'Circuit',
|
||||||
@ -15,7 +15,6 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class CircuitType(OrganizationalModel):
|
class CircuitType(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||||
@ -44,8 +43,7 @@ class CircuitType(OrganizationalModel):
|
|||||||
return reverse('circuits:circuittype', args=[self.pk])
|
return reverse('circuits:circuittype', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Circuit(NetBoxModel):
|
||||||
class Circuit(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||||
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
|
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
|
||||||
@ -138,8 +136,7 @@ class Circuit(PrimaryModel):
|
|||||||
return CircuitStatusChoices.colors.get(self.status, 'secondary')
|
return CircuitStatusChoices.colors.get(self.status, 'secondary')
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
|
||||||
class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
|
||||||
circuit = models.ForeignKey(
|
circuit = models.ForeignKey(
|
||||||
to='circuits.Circuit',
|
to='circuits.Circuit',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -212,13 +209,9 @@ class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
|||||||
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
|
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
# Annotate the parent Circuit
|
objectchange = super().to_objectchange(action)
|
||||||
try:
|
objectchange.related_object = self.circuit
|
||||||
circuit = self.circuit
|
return objectchange
|
||||||
except Circuit.DoesNotExist:
|
|
||||||
# Parent circuit has been deleted
|
|
||||||
circuit = None
|
|
||||||
return super().to_objectchange(action, related_object=circuit)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_object(self):
|
def parent_object(self):
|
||||||
|
@ -3,8 +3,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.utils import extras_features
|
from netbox.models import NetBoxModel
|
||||||
from netbox.models import PrimaryModel
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ProviderNetwork',
|
'ProviderNetwork',
|
||||||
@ -12,8 +11,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Provider(NetBoxModel):
|
||||||
class Provider(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
stores information pertinent to the user's relationship with the Provider.
|
stores information pertinent to the user's relationship with the Provider.
|
||||||
@ -72,8 +70,7 @@ class Provider(PrimaryModel):
|
|||||||
return reverse('circuits:provider', args=[self.pk])
|
return reverse('circuits:provider', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class ProviderNetwork(NetBoxModel):
|
||||||
class ProviderNetwork(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||||
unimportant to the user.
|
unimportant to the user.
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitTable',
|
'CircuitTable',
|
||||||
'CircuitTypeTable',
|
'CircuitTypeTable',
|
||||||
@ -23,12 +22,32 @@ CIRCUITTERMINATION_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderTable(BaseTable):
|
class ProviderTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -36,16 +55,16 @@ class ProviderTable(BaseTable):
|
|||||||
accessor=Accessor('count_circuits'),
|
accessor=Accessor('count_circuits'),
|
||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:provider_list'
|
url_name='circuits:provider_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||||
'comments', 'tags',
|
'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||||
|
|
||||||
@ -54,22 +73,23 @@ class ProviderTable(BaseTable):
|
|||||||
# Provider networks
|
# Provider networks
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderNetworkTable(BaseTable):
|
class ProviderNetworkTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
provider = tables.Column(
|
provider = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:providernetwork_list'
|
url_name='circuits:providernetwork_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
fields = ('pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'tags')
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'provider', 'service_id', 'description')
|
default_columns = ('pk', 'name', 'provider', 'service_id', 'description')
|
||||||
|
|
||||||
|
|
||||||
@ -77,31 +97,30 @@ class ProviderNetworkTable(BaseTable):
|
|||||||
# Circuit types
|
# Circuit types
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeTable(BaseTable):
|
class CircuitTypeTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:circuittype_list'
|
url_name='circuits:circuittype_list'
|
||||||
)
|
)
|
||||||
circuit_count = tables.Column(
|
circuit_count = tables.Column(
|
||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(CircuitType)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
|
fields = (
|
||||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
'pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTable(BaseTable):
|
class CircuitTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
cid = tables.Column(
|
cid = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Circuit ID'
|
verbose_name='Circuit ID'
|
||||||
@ -109,7 +128,7 @@ class CircuitTable(BaseTable):
|
|||||||
provider = tables.Column(
|
provider = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
termination_a = tables.TemplateColumn(
|
termination_a = tables.TemplateColumn(
|
||||||
template_code=CIRCUITTERMINATION_LINK,
|
template_code=CIRCUITTERMINATION_LINK,
|
||||||
@ -119,16 +138,17 @@ class CircuitTable(BaseTable):
|
|||||||
template_code=CIRCUITTERMINATION_LINK,
|
template_code=CIRCUITTERMINATION_LINK,
|
||||||
verbose_name='Side Z'
|
verbose_name='Side Z'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
commit_rate = CommitRateColumn()
|
||||||
tags = TagColumn(
|
comments = columns.MarkdownColumn()
|
||||||
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:circuit_list'
|
url_name='circuits:circuit_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||||
'commit_rate', 'description', 'comments', 'tags',
|
'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||||
|
@ -5,10 +5,9 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||||||
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.tables import paginate_table
|
from netbox.tables import configure_table
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import CircuitTerminationSideChoices
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ class ProviderView(generic.ObjectView):
|
|||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
|
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
|
||||||
paginate_table(circuits_table, request)
|
configure_table(circuits_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'circuits_table': circuits_table,
|
'circuits_table': circuits_table,
|
||||||
@ -96,7 +95,7 @@ class ProviderNetworkView(generic.ObjectView):
|
|||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits)
|
circuits_table = tables.CircuitTable(circuits)
|
||||||
paginate_table(circuits_table, request)
|
configure_table(circuits_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'circuits_table': circuits_table,
|
'circuits_table': circuits_table,
|
||||||
@ -150,7 +149,7 @@ class CircuitTypeView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
||||||
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
|
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
|
||||||
paginate_table(circuits_table, request)
|
configure_table(circuits_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'circuits_table': circuits_table,
|
'circuits_table': circuits_table,
|
||||||
|
@ -6,7 +6,9 @@ from timezone_field.rest_framework import TimeZoneSerializerField
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import (
|
||||||
|
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
||||||
|
)
|
||||||
from ipam.models import ASN, VLAN
|
from ipam.models import ASN, VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
@ -219,7 +221,7 @@ class RackReservationSerializer(PrimaryModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
|
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -719,6 +721,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
|||||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, allow_blank=True, required=False)
|
||||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False)
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
@ -728,6 +731,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
||||||
wireless_lans = SerializedPKRelatedField(
|
wireless_lans = SerializedPKRelatedField(
|
||||||
@ -743,9 +747,9 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
||||||
'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
'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',
|
'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',
|
'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoint',
|
||||||
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
@ -910,7 +914,7 @@ class CableSerializer(PrimaryModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||||
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
'tags', 'custom_fields',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_termination(self, obj, side):
|
def _get_termination(self, obj, side):
|
||||||
@ -1004,7 +1008,10 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
|
||||||
|
'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1023,7 +1030,10 @@ class PowerPanelSerializer(PrimaryModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
fields = [
|
||||||
|
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
|
||||||
|
'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
|
@ -583,7 +583,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
|
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
|
||||||
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
|
@ -793,6 +793,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||||
TYPE_FLEXSTACK = 'cisco-flexstack'
|
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||||
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||||
|
TYPE_STACKWISE80 = 'cisco-stackwise-80'
|
||||||
|
TYPE_STACKWISE160 = 'cisco-stackwise-160'
|
||||||
|
TYPE_STACKWISE320 = 'cisco-stackwise-320'
|
||||||
|
TYPE_STACKWISE480 = 'cisco-stackwise-480'
|
||||||
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||||
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||||
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||||
@ -927,6 +931,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||||
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||||
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||||
|
(TYPE_STACKWISE80, 'Cisco StackWise-80'),
|
||||||
|
(TYPE_STACKWISE160, 'Cisco StackWise-160'),
|
||||||
|
(TYPE_STACKWISE320, 'Cisco StackWise-320'),
|
||||||
|
(TYPE_STACKWISE480, 'Cisco StackWise-480'),
|
||||||
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||||
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||||
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||||
@ -943,6 +951,19 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceDuplexChoices(ChoiceSet):
|
||||||
|
|
||||||
|
DUPLEX_HALF = 'half'
|
||||||
|
DUPLEX_FULL = 'full'
|
||||||
|
DUPLEX_AUTO = 'auto'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(DUPLEX_HALF, 'Half'),
|
||||||
|
(DUPLEX_FULL, 'Full'),
|
||||||
|
(DUPLEX_AUTO, 'Auto'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceModeChoices(ChoiceSet):
|
class InterfaceModeChoices(ChoiceSet):
|
||||||
|
|
||||||
MODE_ACCESS = 'access'
|
MODE_ACCESS = 'access'
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from extras.filters import TagFilter
|
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN, VRF
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||||
)
|
)
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -79,7 +78,6 @@ class RegionFilterSet(OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Parent region (slug)',
|
label='Parent region (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@ -97,14 +95,13 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Parent site group (slug)',
|
label='Parent site group (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -148,7 +145,6 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
queryset=ASN.objects.all(),
|
queryset=ASN.objects.all(),
|
||||||
label='AS (ID)',
|
label='AS (ID)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
@ -225,7 +221,6 @@ class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Location (slug)',
|
label='Location (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
@ -241,14 +236,13 @@ class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -325,7 +319,6 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
serial = django_filters.CharFilter(
|
serial = django_filters.CharFilter(
|
||||||
lookup_expr='iexact'
|
lookup_expr='iexact'
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
@ -346,7 +339,7 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -389,7 +382,6 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label='User (name)',
|
label='User (name)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
@ -407,14 +399,13 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -461,7 +452,6 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
|||||||
method='_device_bays',
|
method='_device_bays',
|
||||||
label='Has device bays',
|
label='Has device bays',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
@ -507,7 +497,7 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.exclude(devicebaytemplates__isnull=value)
|
return queryset.exclude(devicebaytemplates__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeFilterSet(PrimaryModelFilterSet):
|
class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -546,7 +536,6 @@ class ModuleTypeFilterSet(PrimaryModelFilterSet):
|
|||||||
method='_pass_through_ports',
|
method='_pass_through_ports',
|
||||||
label='Has pass-through ports',
|
label='Has pass-through ports',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
@ -732,7 +721,6 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
|||||||
|
|
||||||
|
|
||||||
class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
@ -751,14 +739,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
|
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -916,7 +903,6 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
|||||||
method='_device_bays',
|
method='_device_bays',
|
||||||
label='Has device bays',
|
label='Has device bays',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@ -970,7 +956,7 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
|||||||
return queryset.exclude(devicebays__isnull=value)
|
return queryset.exclude(devicebays__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
class ModuleFilterSet(PrimaryModelFilterSet):
|
class ModuleFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -990,7 +976,6 @@ class ModuleFilterSet(PrimaryModelFilterSet):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
@ -1080,7 +1065,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Virtual Chassis',
|
label='Virtual Chassis',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -1112,7 +1096,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
|||||||
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class ConsolePortFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1123,7 +1107,7 @@ class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
|
|||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class ConsoleServerPortFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1134,7 +1118,7 @@ class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet
|
|||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class PowerPortFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1145,7 +1129,7 @@ class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
|
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class PowerOutletFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1160,7 +1144,7 @@ class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, Cabl
|
|||||||
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class InterfaceFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1196,9 +1180,12 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
label='LAG interface (ID)',
|
label='LAG interface (ID)',
|
||||||
)
|
)
|
||||||
|
speed = MultiValueNumberFilter()
|
||||||
|
duplex = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=InterfaceDuplexChoices
|
||||||
|
)
|
||||||
mac_address = MultiValueMACAddressFilter()
|
mac_address = MultiValueMACAddressFilter()
|
||||||
wwn = MultiValueWWNFilter()
|
wwn = MultiValueWWNFilter()
|
||||||
tag = TagFilter()
|
|
||||||
vlan_id = django_filters.CharFilter(
|
vlan_id = django_filters.CharFilter(
|
||||||
method='filter_vlan_id',
|
method='filter_vlan_id',
|
||||||
label='Assigned VLAN'
|
label='Assigned VLAN'
|
||||||
@ -1217,6 +1204,17 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
rf_channel = django_filters.MultipleChoiceFilter(
|
rf_channel = django_filters.MultipleChoiceFilter(
|
||||||
choices=WirelessChannelChoices
|
choices=WirelessChannelChoices
|
||||||
)
|
)
|
||||||
|
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vrf',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
label='VRF',
|
||||||
|
)
|
||||||
|
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vrf__rd',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
to_field_name='rd',
|
||||||
|
label='VRF (RD)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@ -1273,7 +1271,7 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
}.get(value, queryset.none())
|
}.get(value, queryset.none())
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
class FrontPortFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1284,7 +1282,7 @@ class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
|||||||
fields = ['id', 'name', 'label', 'type', 'color', 'description']
|
fields = ['id', 'name', 'label', 'type', 'color', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
class RearPortFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1295,21 +1293,21 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
|
|||||||
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
class ModuleBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
class DeviceBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1362,14 +1360,13 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1432,7 +1429,6 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
@ -1449,7 +1445,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter).distinct()
|
return queryset.filter(qs_filter).distinct()
|
||||||
|
|
||||||
|
|
||||||
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1490,7 +1486,6 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
|||||||
method='filter_device',
|
method='filter_device',
|
||||||
field_name='device__site__slug'
|
field_name='device__site__slug'
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
@ -1509,7 +1504,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class PowerPanelFilterSet(PrimaryModelFilterSet):
|
class PowerPanelFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1556,7 +1551,6 @@ class PowerPanelFilterSet(PrimaryModelFilterSet):
|
|||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label='Location (ID)',
|
label='Location (ID)',
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
@ -1571,7 +1565,7 @@ class PowerPanelFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1626,7 +1620,6 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
|||||||
choices=PowerFeedStatusChoices,
|
choices=PowerFeedStatusChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
|
@ -72,12 +72,12 @@ class PowerOutletBulkCreateForm(
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceBulkCreateForm(
|
class InterfaceBulkCreateForm(
|
||||||
form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected']),
|
form_from_model(Interface, ['type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected']),
|
||||||
DeviceBulkAddComponentForm
|
DeviceBulkAddComponentForm
|
||||||
):
|
):
|
||||||
model = Interface
|
model = Interface
|
||||||
field_order = (
|
field_order = (
|
||||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
|
'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,11 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||||
from ipam.models import VLAN, ASN
|
from ipam.models import ASN, VLAN, VRF
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
||||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -1028,7 +1028,7 @@ class PowerOutletBulkEditForm(
|
|||||||
|
|
||||||
class InterfaceBulkEditForm(
|
class InterfaceBulkEditForm(
|
||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
]),
|
]),
|
||||||
AddRemoveTagsForm,
|
AddRemoveTagsForm,
|
||||||
@ -1061,7 +1061,13 @@ class InterfaceBulkEditForm(
|
|||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'type': 'lag',
|
'type': 'lag',
|
||||||
}
|
},
|
||||||
|
label='LAG'
|
||||||
|
)
|
||||||
|
speed = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
widget=SelectSpeedWidget(attrs={'readonly': None}),
|
||||||
|
label='Speed'
|
||||||
)
|
)
|
||||||
mgmt_only = forms.NullBooleanField(
|
mgmt_only = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -1080,11 +1086,16 @@ class InterfaceBulkEditForm(
|
|||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
vrf = DynamicModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
|
'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',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -8,6 +8,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelCSVForm
|
from extras.forms import CustomFieldModelCSVForm
|
||||||
|
from ipam.models import VRF
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
@ -617,11 +618,21 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
|||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
help_text='Physical medium'
|
help_text='Physical medium'
|
||||||
)
|
)
|
||||||
|
duplex = CSVChoiceField(
|
||||||
|
choices=InterfaceDuplexChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
mode = CSVChoiceField(
|
mode = CSVChoiceField(
|
||||||
choices=InterfaceModeChoices,
|
choices=InterfaceModeChoices,
|
||||||
required=False,
|
required=False,
|
||||||
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||||
)
|
)
|
||||||
|
vrf = CSVModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='rd',
|
||||||
|
help_text='Assigned VRF'
|
||||||
|
)
|
||||||
rf_role = CSVChoiceField(
|
rf_role = CSVChoiceField(
|
||||||
choices=WirelessRoleChoices,
|
choices=WirelessRoleChoices,
|
||||||
required=False,
|
required=False,
|
||||||
@ -631,8 +642,8 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
|
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'mac_address',
|
||||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'rf_channel_width', 'tx_power',
|
'rf_channel_width', 'tx_power',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN, VRF
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
||||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
|
||||||
)
|
)
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
|||||||
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||||
model = Location
|
model = Location
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q'],
|
['q', 'tag'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'parent_id'],
|
['region_id', 'site_group_id', 'site_id', 'parent_id'],
|
||||||
['tenant_group_id', 'tenant_id'],
|
['tenant_group_id', 'tenant_id'],
|
||||||
]
|
]
|
||||||
@ -678,7 +678,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
|||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['site_id', 'rack_id', 'device_id'],
|
['site_id', 'rack_id', 'device_id'],
|
||||||
['type', 'status', 'color'],
|
['type', 'status', 'color', 'length', 'length_unit'],
|
||||||
['tenant_group_id', 'tenant_id'],
|
['tenant_group_id', 'tenant_id'],
|
||||||
]
|
]
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -703,6 +703,16 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
|||||||
'site_id': '$site_id'
|
'site_id': '$site_id'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_id',
|
||||||
|
'tenant_id': '$tenant_id',
|
||||||
|
'rack_id': '$rack_id',
|
||||||
|
},
|
||||||
|
label=_('Device')
|
||||||
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
choices=add_blank_choice(CableTypeChoices),
|
choices=add_blank_choice(CableTypeChoices),
|
||||||
required=False,
|
required=False,
|
||||||
@ -716,15 +726,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
|||||||
color = ColorField(
|
color = ColorField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
length = forms.IntegerField(
|
||||||
queryset=Device.objects.all(),
|
required=False
|
||||||
required=False,
|
)
|
||||||
query_params={
|
length_unit = forms.ChoiceField(
|
||||||
'site_id': '$site_id',
|
choices=add_blank_choice(CableLengthUnitChoices),
|
||||||
'tenant_id': '$tenant_id',
|
required=False
|
||||||
'rack_id': '$rack_id',
|
|
||||||
},
|
|
||||||
label=_('Device')
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@ -920,7 +927,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
['name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only'],
|
||||||
|
['vrf_id', 'mac_address', 'wwn'],
|
||||||
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
|
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||||
]
|
]
|
||||||
@ -934,6 +942,17 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelectMultiple()
|
widget=StaticSelectMultiple()
|
||||||
)
|
)
|
||||||
|
speed = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label='Select Speed',
|
||||||
|
widget=SelectSpeedWidget(attrs={'readonly': None})
|
||||||
|
)
|
||||||
|
duplex = forms.MultipleChoiceField(
|
||||||
|
choices=InterfaceDuplexChoices,
|
||||||
|
required=False,
|
||||||
|
label='Select Duplex',
|
||||||
|
widget=StaticSelectMultiple()
|
||||||
|
)
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect(
|
widget=StaticSelect(
|
||||||
@ -980,6 +999,11 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
max_value=127
|
max_value=127
|
||||||
)
|
)
|
||||||
|
vrf_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF'
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,12 +9,12 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelForm
|
from extras.forms import CustomFieldModelForm
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
||||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||||
SlugField, StaticSelect,
|
SlugField, StaticSelect, SelectSpeedWidget,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
@ -1261,6 +1261,11 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
'available_on_device': '$device',
|
'available_on_device': '$device',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
vrf = DynamicModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF'
|
||||||
|
)
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -1269,13 +1274,13 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
|
'device', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
|
||||||
'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
|
'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Interface', ('device', 'name', 'type', 'label', 'description', 'tags')),
|
('Interface', ('device', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
|
||||||
('Addressing', ('mac_address', 'wwn')),
|
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||||
@ -1287,6 +1292,8 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
|
'speed': SelectSpeedWidget(),
|
||||||
|
'duplex': StaticSelect(),
|
||||||
'mode': StaticSelect(),
|
'mode': StaticSelect(),
|
||||||
'rf_role': StaticSelect(),
|
'rf_role': StaticSelect(),
|
||||||
'rf_channel': StaticSelect(),
|
'rf_channel': StaticSelect(),
|
||||||
|
20
netbox/dcim/migrations/0149_interface_vrf.py
Normal file
20
netbox/dcim/migrations/0149_interface_vrf.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.2.11 on 2022-01-07 18:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0054_vlangroup_min_max_vids'),
|
||||||
|
('dcim', '0148_inventoryitem_templates'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='vrf',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces', to='ipam.vrf'),
|
||||||
|
),
|
||||||
|
]
|
23
netbox/dcim/migrations/0150_interface_speed_duplex.py
Normal file
23
netbox/dcim/migrations/0150_interface_speed_duplex.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.10 on 2022-01-08 18:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0149_interface_vrf'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='duplex',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='speed',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
274
netbox/dcim/migrations/0151_standardize_id_fields.py
Normal file
274
netbox/dcim/migrations/0151_standardize_id_fields.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0150_interface_speed_duplex'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Model IDs
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cable',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebaytemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicerole',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitemrole',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitemtemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='location',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='manufacturer',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlettemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerpanel',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rackreservation',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rackrole',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='region',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sitegroup',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
|
||||||
|
# GFK IDs
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_a_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_b_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='destination_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='origin_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -11,8 +11,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import PathField
|
from dcim.fields import PathField
|
||||||
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
||||||
from extras.utils import extras_features
|
from netbox.models import NetBoxModel
|
||||||
from netbox.models import BigIDModel, PrimaryModel
|
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
from utilities.utils import to_meters
|
from utilities.utils import to_meters
|
||||||
from .devices import Device
|
from .devices import Device
|
||||||
@ -29,8 +28,7 @@ __all__ = (
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Cable(NetBoxModel):
|
||||||
class Cable(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A physical connection between two endpoints.
|
A physical connection between two endpoints.
|
||||||
"""
|
"""
|
||||||
@ -40,7 +38,7 @@ class Cable(PrimaryModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
termination_a_id = models.PositiveIntegerField()
|
termination_a_id = models.PositiveBigIntegerField()
|
||||||
termination_a = GenericForeignKey(
|
termination_a = GenericForeignKey(
|
||||||
ct_field='termination_a_type',
|
ct_field='termination_a_type',
|
||||||
fk_field='termination_a_id'
|
fk_field='termination_a_id'
|
||||||
@ -51,7 +49,7 @@ class Cable(PrimaryModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
termination_b_id = models.PositiveIntegerField()
|
termination_b_id = models.PositiveBigIntegerField()
|
||||||
termination_b = GenericForeignKey(
|
termination_b = GenericForeignKey(
|
||||||
ct_field='termination_b_type',
|
ct_field='termination_b_type',
|
||||||
fk_field='termination_b_id'
|
fk_field='termination_b_id'
|
||||||
@ -300,7 +298,7 @@ class Cable(PrimaryModel):
|
|||||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
||||||
|
|
||||||
|
|
||||||
class CablePath(BigIDModel):
|
class CablePath(models.Model):
|
||||||
"""
|
"""
|
||||||
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
|
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
|
||||||
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
|
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
|
||||||
@ -329,7 +327,7 @@ class CablePath(BigIDModel):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
origin_id = models.PositiveIntegerField()
|
origin_id = models.PositiveBigIntegerField()
|
||||||
origin = GenericForeignKey(
|
origin = GenericForeignKey(
|
||||||
ct_field='origin_type',
|
ct_field='origin_type',
|
||||||
fk_field='origin_id'
|
fk_field='origin_id'
|
||||||
@ -341,7 +339,7 @@ class CablePath(BigIDModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
destination_id = models.PositiveIntegerField(
|
destination_id = models.PositiveBigIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -7,8 +7,8 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.utils import extras_features
|
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
|
from netbox.models.features import WebhooksMixin
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.mptt import TreeManager
|
from utilities.mptt import TreeManager
|
||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
@ -32,7 +32,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComponentTemplateModel(ChangeLoggedModel):
|
class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
|
||||||
device_type = models.ForeignKey(
|
device_type = models.ForeignKey(
|
||||||
to='dcim.DeviceType',
|
to='dcim.DeviceType',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -70,14 +70,10 @@ class ComponentTemplateModel(ChangeLoggedModel):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def to_objectchange(self, action, related_object=None):
|
def to_objectchange(self, action):
|
||||||
# Annotate the parent DeviceType
|
objectchange = super().to_objectchange(action)
|
||||||
try:
|
objectchange.related_object = self.device_type
|
||||||
device_type = self.device_type
|
return objectchange
|
||||||
except ObjectDoesNotExist:
|
|
||||||
# The parent DeviceType has already been deleted
|
|
||||||
device_type = None
|
|
||||||
return super().to_objectchange(action, related_object=device_type)
|
|
||||||
|
|
||||||
|
|
||||||
class ModularComponentTemplateModel(ComponentTemplateModel):
|
class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||||
@ -102,19 +98,13 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def to_objectchange(self, action, related_object=None):
|
def to_objectchange(self, action):
|
||||||
# Annotate the parent DeviceType or ModuleType
|
objectchange = super().to_objectchange(action)
|
||||||
try:
|
if self.device_type is not None:
|
||||||
if getattr(self, 'device_type'):
|
objectchange.related_object = self.device_type
|
||||||
return super().to_objectchange(action, related_object=self.device_type)
|
elif self.module_type is not None:
|
||||||
except ObjectDoesNotExist:
|
objectchange.related_object = self.module_type
|
||||||
pass
|
return objectchange
|
||||||
try:
|
|
||||||
if getattr(self, 'module_type'):
|
|
||||||
return super().to_objectchange(action, related_object=self.module_type)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
return super().to_objectchange(action)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -135,7 +125,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a ConsolePort to be created for a new Device.
|
A template for a ConsolePort to be created for a new Device.
|
||||||
@ -164,7 +153,6 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a ConsoleServerPort to be created for a new Device.
|
A template for a ConsoleServerPort to be created for a new Device.
|
||||||
@ -193,7 +181,6 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class PowerPortTemplate(ModularComponentTemplateModel):
|
class PowerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a PowerPort to be created for a new Device.
|
A template for a PowerPort to be created for a new Device.
|
||||||
@ -245,7 +232,6 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class PowerOutletTemplate(ModularComponentTemplateModel):
|
class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a PowerOutlet to be created for a new Device.
|
A template for a PowerOutlet to be created for a new Device.
|
||||||
@ -307,7 +293,6 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class InterfaceTemplate(ModularComponentTemplateModel):
|
class InterfaceTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a physical data interface on a new Device.
|
A template for a physical data interface on a new Device.
|
||||||
@ -347,7 +332,6 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
Template for a pass-through port on the front of a new Device.
|
Template for a pass-through port on the front of a new Device.
|
||||||
@ -420,7 +404,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class RearPortTemplate(ModularComponentTemplateModel):
|
class RearPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
Template for a pass-through port on the rear of a new Device.
|
Template for a pass-through port on the rear of a new Device.
|
||||||
@ -460,7 +443,6 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class ModuleBayTemplate(ComponentTemplateModel):
|
class ModuleBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a ModuleBay to be created for a new parent Device.
|
A template for a ModuleBay to be created for a new parent Device.
|
||||||
@ -486,7 +468,6 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class DeviceBayTemplate(ComponentTemplateModel):
|
class DeviceBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a DeviceBay to be created for a new parent Device.
|
A template for a DeviceBay to be created for a new parent Device.
|
||||||
@ -511,7 +492,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
|
||||||
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for an InventoryItem to be created for a new parent Device.
|
A template for an InventoryItem to be created for a new parent Device.
|
||||||
|
@ -11,8 +11,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField, WWNField
|
from dcim.fields import MACAddressField, WWNField
|
||||||
from dcim.svg import CableTraceSVG
|
from dcim.svg import CableTraceSVG
|
||||||
from extras.utils import extras_features
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.mptt import TreeManager
|
from utilities.mptt import TreeManager
|
||||||
@ -40,7 +39,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComponentModel(PrimaryModel):
|
class ComponentModel(NetBoxModel):
|
||||||
"""
|
"""
|
||||||
An abstract model inherited by any model which has a parent Device.
|
An abstract model inherited by any model which has a parent Device.
|
||||||
"""
|
"""
|
||||||
@ -76,13 +75,9 @@ class ComponentModel(PrimaryModel):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
# Annotate the parent Device
|
objectchange = super().to_objectchange(action)
|
||||||
try:
|
objectchange.related_object = self.device
|
||||||
device = self.device
|
return super().to_objectchange(action)
|
||||||
except ObjectDoesNotExist:
|
|
||||||
# The parent Device has already been deleted
|
|
||||||
device = None
|
|
||||||
return super().to_objectchange(action, related_object=device)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_object(self):
|
def parent_object(self):
|
||||||
@ -131,7 +126,7 @@ class LinkTermination(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_link_peer_id = models.PositiveIntegerField(
|
_link_peer_id = models.PositiveBigIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
@ -254,7 +249,6 @@ class PathEndpoint(models.Model):
|
|||||||
# Console components
|
# Console components
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
@ -282,7 +276,6 @@ class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
@ -314,7 +307,6 @@ class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
# Power components
|
# Power components
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
@ -407,7 +399,6 @@ class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
|
class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
@ -522,7 +513,6 @@ class BaseInterface(models.Model):
|
|||||||
return self.fhrp_group_assignments.count()
|
return self.fhrp_group_assignments.count()
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
@ -551,6 +541,16 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
verbose_name='Management only',
|
verbose_name='Management only',
|
||||||
help_text='This interface is used only for out-of-band management'
|
help_text='This interface is used only for out-of-band management'
|
||||||
)
|
)
|
||||||
|
speed = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
duplex = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
choices=InterfaceDuplexChoices
|
||||||
|
)
|
||||||
wwn = WWNField(
|
wwn = WWNField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -616,6 +616,14 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
)
|
)
|
||||||
|
vrf = models.ForeignKey(
|
||||||
|
to='ipam.VRF',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='interfaces',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='VRF'
|
||||||
|
)
|
||||||
ip_addresses = GenericRelation(
|
ip_addresses = GenericRelation(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
@ -785,7 +793,6 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class FrontPort(ModularComponentModel, LinkTermination):
|
class FrontPort(ModularComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
@ -839,7 +846,6 @@ class FrontPort(ModularComponentModel, LinkTermination):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class RearPort(ModularComponentModel, LinkTermination):
|
class RearPort(ModularComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
@ -883,7 +889,6 @@ class RearPort(ModularComponentModel, LinkTermination):
|
|||||||
# Bays
|
# Bays
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class ModuleBay(ComponentModel):
|
class ModuleBay(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
@ -904,7 +909,6 @@ class ModuleBay(ComponentModel):
|
|||||||
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
|
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class DeviceBay(ComponentModel):
|
class DeviceBay(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
@ -955,7 +959,6 @@ class DeviceBay(ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class InventoryItemRole(OrganizationalModel):
|
class InventoryItemRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Inventory items may optionally be assigned a functional role.
|
Inventory items may optionally be assigned a functional role.
|
||||||
@ -986,7 +989,6 @@ class InventoryItemRole(OrganizationalModel):
|
|||||||
return reverse('dcim:inventoryitemrole', args=[self.pk])
|
return reverse('dcim:inventoryitemrole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class InventoryItem(MPTTModel, ComponentModel):
|
class InventoryItem(MPTTModel, ComponentModel):
|
||||||
"""
|
"""
|
||||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||||
|
@ -13,9 +13,8 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.models import ConfigContextModel
|
from extras.models import ConfigContextModel
|
||||||
from extras.querysets import ConfigContextModelQuerySet
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from extras.utils import extras_features
|
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from .device_components import *
|
from .device_components import *
|
||||||
@ -37,7 +36,6 @@ __all__ = (
|
|||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class Manufacturer(OrganizationalModel):
|
class Manufacturer(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||||
@ -70,8 +68,7 @@ class Manufacturer(OrganizationalModel):
|
|||||||
return reverse('dcim:manufacturer', args=[self.pk])
|
return reverse('dcim:manufacturer', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class DeviceType(NetBoxModel):
|
||||||
class DeviceType(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||||
well as high-level functional role(s).
|
well as high-level functional role(s).
|
||||||
@ -353,8 +350,7 @@ class DeviceType(PrimaryModel):
|
|||||||
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class ModuleType(NetBoxModel):
|
||||||
class ModuleType(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
||||||
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
||||||
@ -487,7 +483,6 @@ class ModuleType(PrimaryModel):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class DeviceRole(OrganizationalModel):
|
class DeviceRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||||
@ -525,7 +520,6 @@ class DeviceRole(OrganizationalModel):
|
|||||||
return reverse('dcim:devicerole', args=[self.pk])
|
return reverse('dcim:devicerole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class Platform(OrganizationalModel):
|
class Platform(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||||
@ -575,8 +569,7 @@ class Platform(OrganizationalModel):
|
|||||||
return reverse('dcim:platform', args=[self.pk])
|
return reverse('dcim:platform', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Device(NetBoxModel, ConfigContextModel):
|
||||||
class Device(PrimaryModel, ConfigContextModel):
|
|
||||||
"""
|
"""
|
||||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||||
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
||||||
@ -1012,8 +1005,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
return DeviceStatusChoices.colors.get(self.status, 'secondary')
|
return DeviceStatusChoices.colors.get(self.status, 'secondary')
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Module(NetBoxModel, ConfigContextModel):
|
||||||
class Module(PrimaryModel, ConfigContextModel):
|
|
||||||
"""
|
"""
|
||||||
A Module represents a field-installable component within a Device which may itself hold multiple device components
|
A Module represents a field-installable component within a Device which may itself hold multiple device components
|
||||||
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
|
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
|
||||||
@ -1095,8 +1087,7 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class VirtualChassis(NetBoxModel):
|
||||||
class VirtualChassis(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||||
"""
|
"""
|
||||||
|
@ -6,8 +6,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.utils import extras_features
|
from netbox.models import NetBoxModel
|
||||||
from netbox.models import PrimaryModel
|
|
||||||
from utilities.validators import ExclusionValidator
|
from utilities.validators import ExclusionValidator
|
||||||
from .device_components import LinkTermination, PathEndpoint
|
from .device_components import LinkTermination, PathEndpoint
|
||||||
|
|
||||||
@ -21,8 +20,7 @@ __all__ = (
|
|||||||
# Power
|
# Power
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class PowerPanel(NetBoxModel):
|
||||||
class PowerPanel(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A distribution point for electrical power; e.g. a data center RPP.
|
A distribution point for electrical power; e.g. a data center RPP.
|
||||||
"""
|
"""
|
||||||
@ -68,8 +66,7 @@ class PowerPanel(PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
|
||||||
class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
|
|
||||||
"""
|
"""
|
||||||
An electrical circuit delivered from a PowerPanel.
|
An electrical circuit delivered from a PowerPanel.
|
||||||
"""
|
"""
|
||||||
|
@ -13,9 +13,8 @@ from django.urls import reverse
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.svg import RackElevationSVG
|
from dcim.svg import RackElevationSVG
|
||||||
from extras.utils import extras_features
|
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.utils import array_to_string
|
from utilities.utils import array_to_string
|
||||||
@ -34,7 +33,6 @@ __all__ = (
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class RackRole(OrganizationalModel):
|
class RackRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Racks can be organized by functional role, similar to Devices.
|
Racks can be organized by functional role, similar to Devices.
|
||||||
@ -65,8 +63,7 @@ class RackRole(OrganizationalModel):
|
|||||||
return reverse('dcim:rackrole', args=[self.pk])
|
return reverse('dcim:rackrole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Rack(NetBoxModel):
|
||||||
class Rack(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||||
Each Rack is assigned to a Site and (optionally) a Location.
|
Each Rack is assigned to a Site and (optionally) a Location.
|
||||||
@ -438,8 +435,7 @@ class Rack(PrimaryModel):
|
|||||||
return int(allocated_draw_total / available_power_total * 100)
|
return int(allocated_draw_total / available_power_total * 100)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class RackReservation(NetBoxModel):
|
||||||
class RackReservation(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
One or more reserved units within a Rack.
|
One or more reserved units within a Rack.
|
||||||
"""
|
"""
|
||||||
|
@ -7,9 +7,7 @@ from timezone_field import TimeZoneField
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import ASNField
|
from netbox.models import NestedGroupModel, NetBoxModel
|
||||||
from extras.utils import extras_features
|
|
||||||
from netbox.models import NestedGroupModel, PrimaryModel
|
|
||||||
from utilities.fields import NaturalOrderingField
|
from utilities.fields import NaturalOrderingField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -24,7 +22,6 @@ __all__ = (
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class Region(NestedGroupModel):
|
class Region(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||||
@ -111,7 +108,6 @@ class Region(NestedGroupModel):
|
|||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class SiteGroup(NestedGroupModel):
|
class SiteGroup(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||||
@ -198,8 +194,7 @@ class SiteGroup(NestedGroupModel):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
class Site(NetBoxModel):
|
||||||
class Site(PrimaryModel):
|
|
||||||
"""
|
"""
|
||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
|
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
|
||||||
@ -322,7 +317,6 @@ class Site(PrimaryModel):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
|
||||||
class Location(NestedGroupModel):
|
class Location(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||||
|
@ -19,7 +19,12 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
def get_device_name(device):
|
def get_device_name(device):
|
||||||
return device.name or str(device.device_type)
|
if device.virtual_chassis:
|
||||||
|
return f'{device.virtual_chassis.name}:{device.vc_position}'
|
||||||
|
elif device.name:
|
||||||
|
return device.name
|
||||||
|
else:
|
||||||
|
return str(device.device_type)
|
||||||
|
|
||||||
|
|
||||||
class RackElevationSVG:
|
class RackElevationSVG:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, BooleanColumn
|
from netbox.tables import BaseTable, columns
|
||||||
from dcim.models import ConsolePort, Interface, PowerPort
|
from dcim.models import ConsolePort, Interface, PowerPort
|
||||||
from .cables import *
|
from .cables import *
|
||||||
from .devices import *
|
from .devices import *
|
||||||
@ -36,7 +36,7 @@ class ConsoleConnectionTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Console Port'
|
verbose_name='Console Port'
|
||||||
)
|
)
|
||||||
reachable = BooleanColumn(
|
reachable = columns.BooleanColumn(
|
||||||
accessor=Accessor('_path__is_active'),
|
accessor=Accessor('_path__is_active'),
|
||||||
verbose_name='Reachable'
|
verbose_name='Reachable'
|
||||||
)
|
)
|
||||||
@ -44,7 +44,6 @@ class ConsoleConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||||
exclude = ('id', )
|
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionTable(BaseTable):
|
class PowerConnectionTable(BaseTable):
|
||||||
@ -67,7 +66,7 @@ class PowerConnectionTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Power Port'
|
verbose_name='Power Port'
|
||||||
)
|
)
|
||||||
reachable = BooleanColumn(
|
reachable = columns.BooleanColumn(
|
||||||
accessor=Accessor('_path__is_active'),
|
accessor=Accessor('_path__is_active'),
|
||||||
verbose_name='Reachable'
|
verbose_name='Reachable'
|
||||||
)
|
)
|
||||||
@ -75,7 +74,6 @@ class PowerConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||||
exclude = ('id', )
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionTable(BaseTable):
|
class InterfaceConnectionTable(BaseTable):
|
||||||
@ -101,7 +99,7 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Interface B'
|
verbose_name='Interface B'
|
||||||
)
|
)
|
||||||
reachable = BooleanColumn(
|
reachable = columns.BooleanColumn(
|
||||||
accessor=Accessor('_path__is_active'),
|
accessor=Accessor('_path__is_active'),
|
||||||
verbose_name='Reachable'
|
verbose_name='Reachable'
|
||||||
)
|
)
|
||||||
@ -109,4 +107,3 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||||
exclude = ('id', )
|
|
||||||
|
@ -2,8 +2,8 @@ import django_tables2 as tables
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Cable
|
from dcim.models import Cable
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
|
|
||||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -15,8 +15,7 @@ __all__ = (
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableTable(BaseTable):
|
class CableTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
termination_a_parent = tables.TemplateColumn(
|
termination_a_parent = tables.TemplateColumn(
|
||||||
template_code=CABLE_TERMINATION_PARENT,
|
template_code=CABLE_TERMINATION_PARENT,
|
||||||
accessor=Accessor('termination_a'),
|
accessor=Accessor('termination_a'),
|
||||||
@ -41,22 +40,22 @@ class CableTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Termination B'
|
verbose_name='Termination B'
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
length = TemplateColumn(
|
length = columns.TemplateColumn(
|
||||||
template_code=CABLE_LENGTH,
|
template_code=CABLE_LENGTH,
|
||||||
order_by='_abs_length'
|
order_by='_abs_length'
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:cable_list'
|
url_name='dcim:cable_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||||
'status', 'type', 'tenant', 'color', 'length', 'tags',
|
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||||
|
@ -5,11 +5,8 @@ from dcim.models import (
|
|||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
||||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||||
)
|
)
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
|
||||||
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
|
||||||
)
|
|
||||||
from .template_code import *
|
from .template_code import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -74,69 +71,65 @@ def get_interface_state_attribute(record):
|
|||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleTable(BaseTable):
|
class DeviceRoleTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
device_count = LinkedCountColumn(
|
device_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
vm_count = LinkedCountColumn(
|
vm_count = columns.LinkedCountColumn(
|
||||||
viewname='virtualization:virtualmachine_list',
|
viewname='virtualization:virtualmachine_list',
|
||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
vm_role = BooleanColumn()
|
vm_role = columns.BooleanColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicerole_list'
|
url_name='dcim:devicerole_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(DeviceRole)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
|
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
|
||||||
'actions',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
|
|
||||||
class PlatformTable(BaseTable):
|
class PlatformTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
device_count = LinkedCountColumn(
|
device_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'platform_id': 'pk'},
|
url_params={'platform_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
vm_count = LinkedCountColumn(
|
vm_count = columns.LinkedCountColumn(
|
||||||
viewname='virtualization:virtualmachine_list',
|
viewname='virtualization:virtualmachine_list',
|
||||||
url_params={'platform_id': 'pk'},
|
url_params={'platform_id': 'pk'},
|
||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:platform_list'
|
url_name='dcim:platform_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(Platform)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||||
'description', 'tags', 'actions',
|
'description', 'tags', 'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -144,13 +137,12 @@ class PlatformTable(BaseTable):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTable(BaseTable):
|
class DeviceTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -161,7 +153,7 @@ class DeviceTable(BaseTable):
|
|||||||
rack = tables.Column(
|
rack = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
device_role = ColoredLabelColumn(
|
device_role = columns.ColoredLabelColumn(
|
||||||
verbose_name='Role'
|
verbose_name='Role'
|
||||||
)
|
)
|
||||||
manufacturer = tables.Column(
|
manufacturer = tables.Column(
|
||||||
@ -197,17 +189,18 @@ class DeviceTable(BaseTable):
|
|||||||
vc_priority = tables.Column(
|
vc_priority = tables.Column(
|
||||||
verbose_name='VC Priority'
|
verbose_name='VC Priority'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:device_list'
|
url_name='dcim:device_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
||||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
|
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created',
|
||||||
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||||
@ -215,11 +208,11 @@ class DeviceTable(BaseTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportTable(BaseTable):
|
class DeviceImportTable(NetBoxTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -234,7 +227,7 @@ class DeviceImportTable(BaseTable):
|
|||||||
verbose_name='Type'
|
verbose_name='Type'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||||
empty_text = False
|
empty_text = False
|
||||||
@ -244,8 +237,7 @@ class DeviceImportTable(BaseTable):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceComponentTable(BaseTable):
|
class DeviceComponentTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -254,7 +246,7 @@ class DeviceComponentTable(BaseTable):
|
|||||||
order_by=('_name',)
|
order_by=('_name',)
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
order_by = ('device', 'name')
|
order_by = ('device', 'name')
|
||||||
|
|
||||||
|
|
||||||
@ -271,26 +263,26 @@ class ModularDeviceComponentTable(DeviceComponentTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CableTerminationTable(BaseTable):
|
class CableTerminationTable(NetBoxTable):
|
||||||
cable = tables.Column(
|
cable = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
cable_color = ColorColumn(
|
cable_color = columns.ColorColumn(
|
||||||
accessor='cable.color',
|
accessor='cable.color',
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Cable Color'
|
verbose_name='Cable Color'
|
||||||
)
|
)
|
||||||
link_peer = TemplateColumn(
|
link_peer = columns.TemplateColumn(
|
||||||
accessor='_link_peer',
|
accessor='_link_peer',
|
||||||
template_code=LINKTERMINATION,
|
template_code=LINKTERMINATION,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Link Peer'
|
verbose_name='Link Peer'
|
||||||
)
|
)
|
||||||
mark_connected = BooleanColumn()
|
mark_connected = columns.BooleanColumn()
|
||||||
|
|
||||||
|
|
||||||
class PathEndpointTable(CableTerminationTable):
|
class PathEndpointTable(CableTerminationTable):
|
||||||
connection = TemplateColumn(
|
connection = columns.TemplateColumn(
|
||||||
accessor='_path.last_node',
|
accessor='_path.last_node',
|
||||||
template_code=LINKTERMINATION,
|
template_code=LINKTERMINATION,
|
||||||
verbose_name='Connection',
|
verbose_name='Connection',
|
||||||
@ -305,7 +297,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:consoleport_list'
|
url_name='dcim:consoleport_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -313,7 +305,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||||
|
|
||||||
@ -324,10 +316,8 @@ class DeviceConsolePortTable(ConsolePortTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=ConsolePort,
|
extra_buttons=CONSOLEPORT_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=CONSOLEPORT_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -336,7 +326,7 @@ class DeviceConsolePortTable(ConsolePortTable):
|
|||||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||||
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
|
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
}
|
}
|
||||||
@ -349,7 +339,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:consoleserverport_list'
|
url_name='dcim:consoleserverport_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -357,7 +347,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||||
|
|
||||||
@ -369,10 +359,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=ConsoleServerPort,
|
extra_buttons=CONSOLESERVERPORT_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=CONSOLESERVERPORT_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -381,7 +369,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
|||||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||||
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
}
|
}
|
||||||
@ -394,7 +382,7 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:powerport_list'
|
url_name='dcim:powerport_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -402,7 +390,8 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
|
||||||
'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
|
||||||
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||||
|
|
||||||
@ -414,10 +403,8 @@ class DevicePowerPortTable(PowerPortTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=PowerPort,
|
extra_buttons=POWERPORT_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=POWERPORT_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -428,7 +415,6 @@ class DevicePowerPortTable(PowerPortTable):
|
|||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
||||||
'actions',
|
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
@ -445,7 +431,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
power_port = tables.Column(
|
power_port = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:poweroutlet_list'
|
url_name='dcim:poweroutlet_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -453,7 +439,8 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
|
||||||
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
|
||||||
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||||
|
|
||||||
@ -464,10 +451,8 @@ class DevicePowerOutletTable(PowerOutletTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=PowerOutlet,
|
extra_buttons=POWEROUTLET_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=POWEROUTLET_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -477,15 +462,15 @@ class DevicePowerOutletTable(PowerOutletTable):
|
|||||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
|
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseInterfaceTable(BaseTable):
|
class BaseInterfaceTable(NetBoxTable):
|
||||||
enabled = BooleanColumn()
|
enabled = columns.BooleanColumn()
|
||||||
ip_addresses = tables.TemplateColumn(
|
ip_addresses = tables.TemplateColumn(
|
||||||
template_code=INTERFACE_IPADDRESSES,
|
template_code=INTERFACE_IPADDRESSES,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
@ -498,7 +483,7 @@ class BaseInterfaceTable(BaseTable):
|
|||||||
verbose_name='FHRP Groups'
|
verbose_name='FHRP Groups'
|
||||||
)
|
)
|
||||||
untagged_vlan = tables.Column(linkify=True)
|
untagged_vlan = tables.Column(linkify=True)
|
||||||
tagged_vlans = TemplateColumn(
|
tagged_vlans = columns.TemplateColumn(
|
||||||
template_code=INTERFACE_TAGGED_VLANS,
|
template_code=INTERFACE_TAGGED_VLANS,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
@ -512,16 +497,19 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
mgmt_only = BooleanColumn()
|
mgmt_only = columns.BooleanColumn()
|
||||||
wireless_link = tables.Column(
|
wireless_link = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
wireless_lans = TemplateColumn(
|
wireless_lans = columns.TemplateColumn(
|
||||||
template_code=INTERFACE_WIRELESS_LANS,
|
template_code=INTERFACE_WIRELESS_LANS,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Wireless LANs'
|
verbose_name='Wireless LANs'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
vrf = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:interface_list'
|
url_name='dcim:interface_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -529,9 +517,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||||
'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans',
|
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||||
'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
||||||
|
'tagged_vlans', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@ -554,10 +543,8 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='LAG'
|
verbose_name='LAG'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=Interface,
|
extra_buttons=INTERFACE_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=INTERFACE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -572,7 +559,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
order_by = ('name',)
|
order_by = ('name',)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||||
'cable', 'connection', 'actions',
|
'cable', 'connection',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_interface_row_class,
|
'class': get_interface_row_class,
|
||||||
@ -588,14 +575,14 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
rear_port_position = tables.Column(
|
rear_port_position = tables.Column(
|
||||||
verbose_name='Position'
|
verbose_name='Position'
|
||||||
)
|
)
|
||||||
rear_port = tables.Column(
|
rear_port = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:frontport_list'
|
url_name='dcim:frontport_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -604,6 +591,7 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
|
||||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
|
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||||
@ -617,10 +605,8 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=FrontPort,
|
extra_buttons=FRONTPORT_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=FRONTPORT_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -631,7 +617,6 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
|
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
|
||||||
'actions',
|
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
@ -645,8 +630,8 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rearport_list'
|
url_name='dcim:rearport_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -654,7 +639,7 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
model = RearPort
|
model = RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
|
||||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
|
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||||
|
|
||||||
@ -666,10 +651,8 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=RearPort,
|
extra_buttons=REARPORT_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=REARPORT_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -679,7 +662,7 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions',
|
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_cabletermination_row_class
|
'class': get_cabletermination_row_class
|
||||||
@ -700,13 +683,17 @@ class DeviceBayTable(DeviceComponentTable):
|
|||||||
installed_device = tables.Column(
|
installed_device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicebay_list'
|
url_name='dcim:devicebay_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
|
)
|
||||||
|
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||||
|
|
||||||
|
|
||||||
@ -717,10 +704,8 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=DeviceBay,
|
extra_buttons=DEVICEBAY_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=DEVICEBAY_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
@ -728,9 +713,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
|||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
|
||||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTable(DeviceComponentTable):
|
class ModuleBayTable(DeviceComponentTable):
|
||||||
@ -744,7 +727,7 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Installed module'
|
verbose_name='Installed module'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:modulebay_list'
|
url_name='dcim:modulebay_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -755,16 +738,14 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceModuleBayTable(ModuleBayTable):
|
class DeviceModuleBayTable(ModuleBayTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=DeviceBay,
|
extra_buttons=MODULEBAY_BUTTONS
|
||||||
buttons=('edit', 'delete'),
|
|
||||||
prepend_template=MODULEBAY_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
|
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'label', 'description', 'installed_module', 'actions')
|
default_columns = ('pk', 'name', 'label', 'description', 'installed_module')
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTable(DeviceComponentTable):
|
class InventoryItemTable(DeviceComponentTable):
|
||||||
@ -785,17 +766,17 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
discovered = BooleanColumn()
|
discovered = columns.BooleanColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:inventoryitem_list'
|
url_name='dcim:inventoryitem_list'
|
||||||
)
|
)
|
||||||
cable = None # Override DeviceComponentTable
|
cable = None # Override DeviceComponentTable
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||||
'asset_tag', 'description', 'discovered', 'tags',
|
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||||
@ -809,68 +790,62 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
|||||||
order_by=Accessor('_name'),
|
order_by=Accessor('_name'),
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn()
|
||||||
model=InventoryItem,
|
|
||||||
buttons=('edit', 'delete')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||||
'description', 'discovered', 'tags', 'actions',
|
'description', 'discovered', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions',
|
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemRoleTable(BaseTable):
|
class InventoryItemRoleTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
inventoryitem_count = LinkedCountColumn(
|
inventoryitem_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:inventoryitem_list',
|
viewname='dcim:inventoryitem_list',
|
||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='Items'
|
verbose_name='Items'
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:inventoryitemrole_list'
|
url_name='dcim:inventoryitemrole_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(InventoryItemRole)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
|
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description', 'actions')
|
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisTable(BaseTable):
|
class VirtualChassisTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
master = tables.Column(
|
master = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
member_count = LinkedCountColumn(
|
member_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'virtual_chassis_id': 'pk'},
|
url_params={'virtual_chassis_id': 'pk'},
|
||||||
verbose_name='Members'
|
verbose_name='Members'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:virtualchassis_list'
|
url_name='dcim:virtualchassis_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
|
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
|
||||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||||
|
@ -5,9 +5,7 @@ from dcim.models import (
|
|||||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
||||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
)
|
)
|
||||||
from utilities.tables import (
|
from netbox.tables import NetBoxTable, columns
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
|
|
||||||
)
|
|
||||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -30,8 +28,7 @@ __all__ = (
|
|||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerTable(BaseTable):
|
class ManufacturerTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -45,19 +42,18 @@ class ManufacturerTable(BaseTable):
|
|||||||
verbose_name='Platforms'
|
verbose_name='Platforms'
|
||||||
)
|
)
|
||||||
slug = tables.Column()
|
slug = tables.Column()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:manufacturer_list'
|
url_name='dcim:manufacturer_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(Manufacturer)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||||
'actions',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -65,30 +61,29 @@ class ManufacturerTable(BaseTable):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeTable(BaseTable):
|
class DeviceTypeTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
model = tables.Column(
|
model = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Device Type'
|
verbose_name='Device Type'
|
||||||
)
|
)
|
||||||
is_full_depth = BooleanColumn(
|
is_full_depth = columns.BooleanColumn(
|
||||||
verbose_name='Full Depth'
|
verbose_name='Full Depth'
|
||||||
)
|
)
|
||||||
instance_count = LinkedCountColumn(
|
instance_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'device_type_id': 'pk'},
|
url_params={'device_type_id': 'pk'},
|
||||||
verbose_name='Instances'
|
verbose_name='Instances'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicetype_list'
|
url_name='dcim:devicetype_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||||
'airflow', 'comments', 'instance_count', 'tags',
|
'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||||
@ -99,8 +94,7 @@ class DeviceTypeTable(BaseTable):
|
|||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ComponentTemplateTable(BaseTable):
|
class ComponentTemplateTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
id = tables.Column(
|
id = tables.Column(
|
||||||
verbose_name='ID'
|
verbose_name='ID'
|
||||||
)
|
)
|
||||||
@ -108,15 +102,14 @@ class ComponentTemplateTable(BaseTable):
|
|||||||
order_by=('_name',)
|
order_by=('_name',)
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
exclude = ('id', )
|
exclude = ('id', )
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=ConsolePortTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -126,10 +119,9 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=ConsoleServerPortTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -139,10 +131,9 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=PowerPortTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -152,10 +143,9 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=PowerOutletTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -165,13 +155,12 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateTable(ComponentTemplateTable):
|
class InterfaceTemplateTable(ComponentTemplateTable):
|
||||||
mgmt_only = BooleanColumn(
|
mgmt_only = columns.BooleanColumn(
|
||||||
verbose_name='Management Only'
|
verbose_name='Management Only'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=InterfaceTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -184,11 +173,10 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
rear_port_position = tables.Column(
|
rear_port_position = tables.Column(
|
||||||
verbose_name='Position'
|
verbose_name='Position'
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=FrontPortTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -198,11 +186,10 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class RearPortTemplateTable(ComponentTemplateTable):
|
class RearPortTemplateTable(ComponentTemplateTable):
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=RearPortTemplate,
|
sequence=('edit', 'delete'),
|
||||||
buttons=('edit', 'delete'),
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -212,9 +199,8 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateTable(ComponentTemplateTable):
|
class ModuleBayTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=ModuleBayTemplate,
|
sequence=('edit', 'delete')
|
||||||
buttons=('edit', 'delete')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -224,9 +210,8 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=DeviceBayTemplate,
|
sequence=('edit', 'delete')
|
||||||
buttons=('edit', 'delete')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
@ -236,9 +221,8 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateTable(ComponentTemplateTable):
|
class InventoryItemTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=InventoryItemTemplate,
|
sequence=('edit', 'delete')
|
||||||
buttons=('edit', 'delete')
|
|
||||||
)
|
)
|
||||||
role = tables.Column(
|
role = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import Module, ModuleType
|
from dcim.models import Module, ModuleType
|
||||||
from utilities.tables import BaseTable, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ModuleTable',
|
'ModuleTable',
|
||||||
@ -9,23 +9,22 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeTable(BaseTable):
|
class ModuleTypeTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
model = tables.Column(
|
model = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Module Type'
|
verbose_name='Module Type'
|
||||||
)
|
)
|
||||||
instance_count = LinkedCountColumn(
|
instance_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:module_list',
|
viewname='dcim:module_list',
|
||||||
url_params={'module_type_id': 'pk'},
|
url_params={'module_type_id': 'pk'},
|
||||||
verbose_name='Instances'
|
verbose_name='Instances'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:moduletype_list'
|
url_name='dcim:moduletype_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
||||||
@ -35,8 +34,7 @@ class ModuleTypeTable(BaseTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTable(BaseTable):
|
class ModuleTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -46,12 +44,12 @@ class ModuleTable(BaseTable):
|
|||||||
module_type = tables.Column(
|
module_type = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:module_list'
|
url_name='dcim:module_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Module
|
model = Module
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import PowerFeed, PowerPanel
|
from dcim.models import PowerFeed, PowerPanel
|
||||||
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn
|
from netbox.tables import NetBoxTable, columns
|
||||||
from .devices import CableTerminationTable
|
from .devices import CableTerminationTable
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -14,26 +14,25 @@ __all__ = (
|
|||||||
# Power panels
|
# Power panels
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelTable(BaseTable):
|
class PowerPanelTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
powerfeed_count = LinkedCountColumn(
|
powerfeed_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:powerfeed_list',
|
viewname='dcim:powerfeed_list',
|
||||||
url_params={'power_panel_id': 'pk'},
|
url_params={'power_panel_id': 'pk'},
|
||||||
verbose_name='Feeds'
|
verbose_name='Feeds'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:powerpanel_list'
|
url_name='dcim:powerpanel_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',)
|
||||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||||
|
|
||||||
|
|
||||||
@ -44,7 +43,6 @@ class PowerPanelTable(BaseTable):
|
|||||||
# We're not using PathEndpointTable for PowerFeed because power connections
|
# We're not using PathEndpointTable for PowerFeed because power connections
|
||||||
# cannot traverse pass-through ports.
|
# cannot traverse pass-through ports.
|
||||||
class PowerFeedTable(CableTerminationTable):
|
class PowerFeedTable(CableTerminationTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -54,25 +52,25 @@ class PowerFeedTable(CableTerminationTable):
|
|||||||
rack = tables.Column(
|
rack = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
type = ChoiceFieldColumn()
|
type = columns.ChoiceFieldColumn()
|
||||||
max_utilization = tables.TemplateColumn(
|
max_utilization = tables.TemplateColumn(
|
||||||
template_code="{{ value }}%"
|
template_code="{{ value }}%"
|
||||||
)
|
)
|
||||||
available_power = tables.Column(
|
available_power = tables.Column(
|
||||||
verbose_name='Available power (VA)'
|
verbose_name='Available power (VA)'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:powerfeed_list'
|
url_name='dcim:powerfeed_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
|
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
|
||||||
'comments', 'tags',
|
'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||||
|
@ -2,11 +2,8 @@ import django_tables2 as tables
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Rack, RackReservation, RackRole
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
|
||||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn,
|
|
||||||
TagColumn, ToggleColumn, UtilizationColumn,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RackTable',
|
'RackTable',
|
||||||
@ -19,28 +16,28 @@ __all__ = (
|
|||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleTable(BaseTable):
|
class RackRoleTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(linkify=True)
|
name = tables.Column(linkify=True)
|
||||||
rack_count = tables.Column(verbose_name='Racks')
|
rack_count = tables.Column(verbose_name='Racks')
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rackrole_list'
|
url_name='dcim:rackrole_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(RackRole)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
|
fields = (
|
||||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', 'created',
|
||||||
|
'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackTable(BaseTable):
|
class RackTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -52,27 +49,27 @@ class RackTable(BaseTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
role = ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
u_height = tables.TemplateColumn(
|
u_height = tables.TemplateColumn(
|
||||||
template_code="{{ record.u_height }}U",
|
template_code="{{ record.u_height }}U",
|
||||||
verbose_name='Height'
|
verbose_name='Height'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
device_count = LinkedCountColumn(
|
device_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'rack_id': 'pk'},
|
url_params={'rack_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
get_utilization = UtilizationColumn(
|
get_utilization = columns.UtilizationColumn(
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Space'
|
verbose_name='Space'
|
||||||
)
|
)
|
||||||
get_power_utilization = UtilizationColumn(
|
get_power_utilization = columns.UtilizationColumn(
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Power'
|
verbose_name='Power'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rack_list'
|
url_name='dcim:rack_list'
|
||||||
)
|
)
|
||||||
outer_width = tables.TemplateColumn(
|
outer_width = tables.TemplateColumn(
|
||||||
@ -84,11 +81,12 @@ class RackTable(BaseTable):
|
|||||||
verbose_name='Outer Depth'
|
verbose_name='Outer Depth'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
|
||||||
'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
||||||
|
'get_power_utilization', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||||
@ -100,8 +98,7 @@ class RackTable(BaseTable):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationTable(BaseTable):
|
class RackReservationTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
reservation = tables.Column(
|
reservation = tables.Column(
|
||||||
accessor='pk',
|
accessor='pk',
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -118,17 +115,14 @@ class RackReservationTable(BaseTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Units'
|
verbose_name='Units'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rackreservation_list'
|
url_name='dcim:rackreservation_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(RackReservation)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||||
'actions',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
|
||||||
default_columns = (
|
|
||||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
|
|
||||||
)
|
)
|
||||||
|
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
from .template_code import LOCATION_BUTTONS
|
||||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn,
|
|
||||||
)
|
|
||||||
from .template_code import LOCATION_ELEVATIONS
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'LocationTable',
|
'LocationTable',
|
||||||
@ -19,85 +17,85 @@ __all__ = (
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class RegionTable(BaseTable):
|
class RegionTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
name = columns.MPTTColumn(
|
||||||
name = MPTTColumn(
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
site_count = LinkedCountColumn(
|
site_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:site_list',
|
viewname='dcim:site_list',
|
||||||
url_params={'region_id': 'pk'},
|
url_params={'region_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:region_list'
|
url_name='dcim:region_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(Region)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Region
|
model = Region
|
||||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
fields = (
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteGroupTable(BaseTable):
|
class SiteGroupTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
name = columns.MPTTColumn(
|
||||||
name = MPTTColumn(
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
site_count = LinkedCountColumn(
|
site_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:site_list',
|
viewname='dcim:site_list',
|
||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:sitegroup_list'
|
url_name='dcim:sitegroup_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(SiteGroup)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
fields = (
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteTable(BaseTable):
|
class SiteTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
region = tables.Column(
|
region = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
asn_count = LinkedCountColumn(
|
asn_count = columns.LinkedCountColumn(
|
||||||
accessor=tables.A('asns.count'),
|
accessor=tables.A('asns.count'),
|
||||||
viewname='ipam:asn_list',
|
viewname='ipam:asn_list',
|
||||||
url_params={'site_id': 'pk'},
|
url_params={'site_id': 'pk'},
|
||||||
verbose_name='ASNs'
|
verbose_name='ASNs'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:site_list'
|
url_name='dcim:site_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
||||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
|
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
|
||||||
|
'created', 'last_updated', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
||||||
|
|
||||||
@ -106,37 +104,35 @@ class SiteTable(BaseTable):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
class LocationTable(BaseTable):
|
class LocationTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
name = columns.MPTTColumn(
|
||||||
name = MPTTColumn(
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
rack_count = LinkedCountColumn(
|
rack_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:rack_list',
|
viewname='dcim:rack_list',
|
||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Racks'
|
verbose_name='Racks'
|
||||||
)
|
)
|
||||||
device_count = LinkedCountColumn(
|
device_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:device_list',
|
viewname='dcim:device_list',
|
||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:location_list'
|
url_name='dcim:location_list'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = columns.ActionsColumn(
|
||||||
model=Location,
|
extra_buttons=LOCATION_BUTTONS
|
||||||
prepend_template=LOCATION_ELEVATIONS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Location
|
model = Location
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
|
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
|
||||||
'actions',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
||||||
|
@ -87,7 +87,7 @@ POWERFEED_CABLETERMINATION = """
|
|||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LOCATION_ELEVATIONS = """
|
LOCATION_BUTTONS = """
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||||
<i class="mdi mdi-server"></i>
|
<i class="mdi mdi-server"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -99,8 +99,8 @@ LOCATION_ELEVATIONS = """
|
|||||||
|
|
||||||
MODULAR_COMPONENT_TEMPLATE_BUTTONS = """
|
MODULAR_COMPONENT_TEMPLATE_BUTTONS = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% if perms.dcim.add_invnetoryitemtemplate %}
|
{% if perms.dcim.add_inventoryitemtemplate %}
|
||||||
<a href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ record.device_type.pk }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={{ request.path }}" title="Add inventory item" class="btn btn-primary btn-sm">
|
<a href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ record.device_type_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={{ request.path }}" title="Add inventory item" class="btn btn-primary btn-sm">
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -6,7 +6,7 @@ from rest_framework import status
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, RIR, VLAN
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
@ -1424,6 +1424,13 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
)
|
)
|
||||||
WirelessLAN.objects.bulk_create(wireless_lans)
|
WirelessLAN.objects.bulk_create(wireless_lans)
|
||||||
|
|
||||||
|
vrfs = (
|
||||||
|
VRF(name='VRF 1'),
|
||||||
|
VRF(name='VRF 2'),
|
||||||
|
VRF(name='VRF 3'),
|
||||||
|
)
|
||||||
|
VRF.objects.bulk_create(vrfs)
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
@ -1431,9 +1438,12 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
'type': '1000base-t',
|
'type': '1000base-t',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
'tx_power': 10,
|
'tx_power': 10,
|
||||||
|
'vrf': vrfs[0].pk,
|
||||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||||
'untagged_vlan': vlans[2].pk,
|
'untagged_vlan': vlans[2].pk,
|
||||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||||
|
'speed': 1000000,
|
||||||
|
'duplex': 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
@ -1442,9 +1452,12 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
'bridge': interfaces[0].pk,
|
'bridge': interfaces[0].pk,
|
||||||
'tx_power': 10,
|
'tx_power': 10,
|
||||||
|
'vrf': vrfs[1].pk,
|
||||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||||
'untagged_vlan': vlans[2].pk,
|
'untagged_vlan': vlans[2].pk,
|
||||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||||
|
'speed': 100000,
|
||||||
|
'duplex': 'half'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
@ -1453,6 +1466,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
'parent': interfaces[1].pk,
|
'parent': interfaces[1].pk,
|
||||||
'tx_power': 10,
|
'tx_power': 10,
|
||||||
|
'vrf': vrfs[2].pk,
|
||||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||||
'untagged_vlan': vlans[2].pk,
|
'untagged_vlan': vlans[2].pk,
|
||||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||||
|
@ -4,7 +4,7 @@ from django.test import TestCase
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.filtersets import *
|
from dcim.filtersets import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, IPAddress, RIR
|
from ipam.models import ASN, IPAddress, RIR, VRF
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
@ -2370,16 +2370,23 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
vrfs = (
|
||||||
|
VRF(name='VRF 1', rd='65000:1'),
|
||||||
|
VRF(name='VRF 2', rd='65000:2'),
|
||||||
|
VRF(name='VRF 3', rd='65000:3'),
|
||||||
|
)
|
||||||
|
VRF.objects.bulk_create(vrfs)
|
||||||
|
|
||||||
# VirtualChassis assignment for filtering
|
# VirtualChassis assignment for filtering
|
||||||
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
|
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
|
||||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
|
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
|
||||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
|
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First', vrf=vrfs[0], speed=1000000, duplex='half'),
|
||||||
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
|
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second', vrf=vrfs[1], speed=1000000, duplex='full'),
|
||||||
Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
|
Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third', vrf=vrfs[2], speed=100000, duplex='half'),
|
||||||
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
|
Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40, speed=100000, duplex='full'),
|
||||||
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
|
Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
|
||||||
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40),
|
Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40),
|
||||||
Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22),
|
Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22),
|
||||||
@ -2416,6 +2423,14 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'mtu': [100, 200]}
|
params = {'mtu': [100, 200]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_speed(self):
|
||||||
|
params = {'speed': [1000000, 100000]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_duplex(self):
|
||||||
|
params = {'duplex': ['half', 'full']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_mgmt_only(self):
|
def test_mgmt_only(self):
|
||||||
params = {'mgmt_only': 'true'}
|
params = {'mgmt_only': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
@ -2550,6 +2565,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tx_power': [40]}
|
params = {'tx_power': [40]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
def test_vrf(self):
|
||||||
|
vrfs = VRF.objects.all()[:2]
|
||||||
|
params = {'vrf_id': [vrfs[0].pk, vrfs[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
|
@ -11,7 +11,7 @@ from netaddr import EUI
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, RIR, VLAN
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
@ -2105,6 +2105,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
)
|
)
|
||||||
WirelessLAN.objects.bulk_create(wireless_lans)
|
WirelessLAN.objects.bulk_create(wireless_lans)
|
||||||
|
|
||||||
|
vrfs = (
|
||||||
|
VRF(name='VRF 1'),
|
||||||
|
VRF(name='VRF 2'),
|
||||||
|
VRF(name='VRF 3'),
|
||||||
|
)
|
||||||
|
VRF.objects.bulk_create(vrfs)
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
@ -2117,6 +2124,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
'mac_address': EUI('01:02:03:04:05:06'),
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 65000,
|
'mtu': 65000,
|
||||||
|
'speed': 1000000,
|
||||||
|
'duplex': 'full',
|
||||||
'mgmt_only': True,
|
'mgmt_only': True,
|
||||||
'description': 'A front port',
|
'description': 'A front port',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
@ -2124,6 +2133,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'untagged_vlan': vlans[0].pk,
|
'untagged_vlan': vlans[0].pk,
|
||||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||||
|
'vrf': vrfs[0].pk,
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2137,12 +2147,15 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
'mac_address': EUI('01:02:03:04:05:06'),
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
|
'speed': 100000,
|
||||||
|
'duplex': 'half',
|
||||||
'mgmt_only': True,
|
'mgmt_only': True,
|
||||||
'description': 'A front port',
|
'description': 'A front port',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
'untagged_vlan': vlans[0].pk,
|
'untagged_vlan': vlans[0].pk,
|
||||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||||
|
'vrf': vrfs[0].pk,
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2153,19 +2166,22 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
'mac_address': EUI('01:02:03:04:05:06'),
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
|
'speed': 1000000,
|
||||||
|
'duplex': 'full',
|
||||||
'mgmt_only': True,
|
'mgmt_only': True,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
'tx_power': 10,
|
'tx_power': 10,
|
||||||
'untagged_vlan': vlans[0].pk,
|
'untagged_vlan': vlans[0].pk,
|
||||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||||
|
'vrf': vrfs[1].pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device,name,type",
|
f"device,name,type,vrf.pk",
|
||||||
"Device 1,Interface 4,1000base-t",
|
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk}",
|
||||||
"Device 1,Interface 5,1000base-t",
|
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk}",
|
||||||
"Device 1,Interface 6,1000base-t",
|
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
@ -20,7 +20,7 @@ from netbox.views import generic
|
|||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.tables import paginate_table
|
from netbox.tables import configure_table
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
@ -165,7 +165,7 @@ class RegionView(generic.ObjectView):
|
|||||||
region=instance
|
region=instance
|
||||||
)
|
)
|
||||||
sites_table = tables.SiteTable(sites, exclude=('region',))
|
sites_table = tables.SiteTable(sites, exclude=('region',))
|
||||||
paginate_table(sites_table, request)
|
configure_table(sites_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'child_regions_table': child_regions_table,
|
'child_regions_table': child_regions_table,
|
||||||
@ -250,7 +250,7 @@ class SiteGroupView(generic.ObjectView):
|
|||||||
group=instance
|
group=instance
|
||||||
)
|
)
|
||||||
sites_table = tables.SiteTable(sites, exclude=('group',))
|
sites_table = tables.SiteTable(sites, exclude=('group',))
|
||||||
paginate_table(sites_table, request)
|
configure_table(sites_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'child_groups_table': child_groups_table,
|
'child_groups_table': child_groups_table,
|
||||||
@ -422,7 +422,7 @@ class LocationView(generic.ObjectView):
|
|||||||
cumulative=True
|
cumulative=True
|
||||||
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
||||||
child_locations_table = tables.LocationTable(child_locations)
|
child_locations_table = tables.LocationTable(child_locations)
|
||||||
paginate_table(child_locations_table, request)
|
configure_table(child_locations_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rack_count': rack_count,
|
'rack_count': rack_count,
|
||||||
@ -493,7 +493,7 @@ class RackRoleView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
|
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
|
||||||
paginate_table(racks_table, request)
|
configure_table(racks_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'racks_table': racks_table,
|
'racks_table': racks_table,
|
||||||
@ -743,7 +743,7 @@ class ManufacturerView(generic.ObjectView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',))
|
devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',))
|
||||||
paginate_table(devicetypes_table, request)
|
configure_table(devicetypes_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'devicetypes_table': devicetypes_table,
|
'devicetypes_table': devicetypes_table,
|
||||||
@ -1439,7 +1439,7 @@ class DeviceRoleView(generic.ObjectView):
|
|||||||
device_role=instance
|
device_role=instance
|
||||||
)
|
)
|
||||||
devices_table = tables.DeviceTable(devices, exclude=('device_role',))
|
devices_table = tables.DeviceTable(devices, exclude=('device_role',))
|
||||||
paginate_table(devices_table, request)
|
configure_table(devices_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'devices_table': devices_table,
|
'devices_table': devices_table,
|
||||||
@ -1503,7 +1503,7 @@ class PlatformView(generic.ObjectView):
|
|||||||
platform=instance
|
platform=instance
|
||||||
)
|
)
|
||||||
devices_table = tables.DeviceTable(devices, exclude=('platform',))
|
devices_table = tables.DeviceTable(devices, exclude=('platform',))
|
||||||
paginate_table(devices_table, request)
|
configure_table(devices_table, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'devices_table': devices_table,
|
'devices_table': devices_table,
|
||||||
@ -2379,8 +2379,9 @@ class DeviceBayPopulateView(generic.ObjectEditView):
|
|||||||
device_bay.installed_device = form.cleaned_data['installed_device']
|
device_bay.installed_device = form.cleaned_data['installed_device']
|
||||||
device_bay.save()
|
device_bay.save()
|
||||||
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||||
|
return_url = self.get_return_url(request)
|
||||||
|
|
||||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
return redirect(return_url)
|
||||||
|
|
||||||
return render(request, 'dcim/devicebay_populate.html', {
|
return render(request, 'dcim/devicebay_populate.html', {
|
||||||
'device_bay': device_bay,
|
'device_bay': device_bay,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
|
|
||||||
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
|
|
||||||
|
|
||||||
@ -44,9 +45,20 @@ class CustomFieldsDataField(Field):
|
|||||||
return self._custom_fields
|
return self._custom_fields
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
return {
|
# TODO: Fix circular import
|
||||||
cf.name: obj.get(cf.name) for cf in self._get_custom_fields()
|
from utilities.api import get_serializer_for_model
|
||||||
}
|
data = {}
|
||||||
|
for cf in self._get_custom_fields():
|
||||||
|
value = cf.deserialize(obj.get(cf.name))
|
||||||
|
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
|
||||||
|
value = serializer(value, context=self.parent.context).data
|
||||||
|
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
|
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
|
||||||
|
value = serializer(value, many=True, context=self.parent.context).data
|
||||||
|
data[cf.name] = value
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
# If updating an existing instance, start with existing custom_field_data
|
# If updating an existing instance, start with existing custom_field_data
|
||||||
|
@ -63,7 +63,7 @@ class WebhookSerializer(ValidatedModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||||
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||||
'conditions', 'ssl_verification', 'ca_file_path',
|
'conditions', 'ssl_verification', 'ca_file_path', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -79,14 +79,28 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
type = ChoiceField(choices=CustomFieldTypeChoices)
|
type = ChoiceField(choices=CustomFieldTypeChoices)
|
||||||
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
||||||
|
data_type = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
'id', 'url', 'display', 'content_types', 'type', 'data_type', 'name', 'label', 'description', 'required',
|
||||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
|
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||||
|
'choices', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_data_type(self, obj):
|
||||||
|
types = CustomFieldTypeChoices
|
||||||
|
if obj.type == types.TYPE_INTEGER:
|
||||||
|
return 'integer'
|
||||||
|
if obj.type == types.TYPE_BOOLEAN:
|
||||||
|
return 'boolean'
|
||||||
|
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
||||||
|
return 'object'
|
||||||
|
if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
|
||||||
|
return 'array'
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom links
|
# Custom links
|
||||||
@ -101,8 +115,8 @@ class CustomLinkSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name',
|
'id', 'url', 'display', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||||
'button_class', 'new_window',
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +134,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
|||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
||||||
'file_extension', 'as_attachment',
|
'file_extension', 'as_attachment', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -134,7 +148,9 @@ class TagSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items']
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -16,6 +16,8 @@ class CustomFieldTypeChoices(ChoiceSet):
|
|||||||
TYPE_JSON = 'json'
|
TYPE_JSON = 'json'
|
||||||
TYPE_SELECT = 'select'
|
TYPE_SELECT = 'select'
|
||||||
TYPE_MULTISELECT = 'multiselect'
|
TYPE_MULTISELECT = 'multiselect'
|
||||||
|
TYPE_OBJECT = 'object'
|
||||||
|
TYPE_MULTIOBJECT = 'multiobject'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(TYPE_TEXT, 'Text'),
|
(TYPE_TEXT, 'Text'),
|
||||||
@ -27,6 +29,8 @@ class CustomFieldTypeChoices(ChoiceSet):
|
|||||||
(TYPE_JSON, 'JSON'),
|
(TYPE_JSON, 'JSON'),
|
||||||
(TYPE_SELECT, 'Selection'),
|
(TYPE_SELECT, 'Selection'),
|
||||||
(TYPE_MULTISELECT, 'Multiple selection'),
|
(TYPE_MULTISELECT, 'Multiple selection'),
|
||||||
|
(TYPE_OBJECT, 'Object'),
|
||||||
|
(TYPE_MULTIOBJECT, 'Multiple objects'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ EXTRAS_FEATURES = [
|
|||||||
'custom_links',
|
'custom_links',
|
||||||
'export_templates',
|
'export_templates',
|
||||||
'job_results',
|
'job_results',
|
||||||
|
'journaling',
|
||||||
'tags',
|
'tags',
|
||||||
'webhooks'
|
'webhooks'
|
||||||
]
|
]
|
||||||
|
@ -82,7 +82,9 @@ class CustomLinkFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
fields = [
|
||||||
|
'id', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
||||||
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -47,6 +47,10 @@ class CustomLinkBulkEditForm(BulkEditForm):
|
|||||||
limit_choices_to=FeatureQuery('custom_fields'),
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
new_window = forms.NullBooleanField(
|
new_window = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
@ -51,7 +51,8 @@ class CustomLinkCSVForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url',
|
'name', 'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
|
||||||
|
'link_url',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, CSVModelForm, FilterForm
|
from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomFieldModelCSVForm',
|
'CustomFieldModelCSVForm',
|
||||||
@ -20,7 +20,7 @@ class CustomFieldsMixin:
|
|||||||
Extend a Form to include custom field support.
|
Extend a Form to include custom field support.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.custom_fields = []
|
self.custom_fields = {}
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -34,6 +34,9 @@ class CustomFieldsMixin:
|
|||||||
raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
|
raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
|
||||||
return ContentType.objects.get_for_model(self.model)
|
return ContentType.objects.get_for_model(self.model)
|
||||||
|
|
||||||
|
def _get_custom_fields(self, content_type):
|
||||||
|
return CustomField.objects.filter(content_types=content_type)
|
||||||
|
|
||||||
def _get_form_field(self, customfield):
|
def _get_form_field(self, customfield):
|
||||||
return customfield.to_form_field()
|
return customfield.to_form_field()
|
||||||
|
|
||||||
@ -41,15 +44,12 @@ class CustomFieldsMixin:
|
|||||||
"""
|
"""
|
||||||
Append form fields for all CustomFields assigned to this object type.
|
Append form fields for all CustomFields assigned to this object type.
|
||||||
"""
|
"""
|
||||||
content_type = self._get_content_type()
|
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||||
|
|
||||||
# Append form fields; assign initial values if modifying and existing object
|
|
||||||
for customfield in CustomField.objects.filter(content_types=content_type):
|
|
||||||
field_name = f'cf_{customfield.name}'
|
field_name = f'cf_{customfield.name}'
|
||||||
self.fields[field_name] = self._get_form_field(customfield)
|
self.fields[field_name] = self._get_form_field(customfield)
|
||||||
|
|
||||||
# Annotate the field in the list of CustomField form fields
|
# Annotate the field in the list of CustomField form fields
|
||||||
self.custom_fields.append(field_name)
|
self.custom_fields[field_name] = customfield
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
|
class CustomFieldModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
|
||||||
@ -70,12 +70,15 @@ class CustomFieldModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Save custom field data on instance
|
# Save custom field data on instance
|
||||||
for cf_name in self.custom_fields:
|
for cf_name, customfield in self.custom_fields.items():
|
||||||
key = cf_name[3:] # Strip "cf_" from field name
|
key = cf_name[3:] # Strip "cf_" from field name
|
||||||
value = self.cleaned_data.get(cf_name)
|
value = self.cleaned_data.get(cf_name)
|
||||||
empty_values = self.fields[cf_name].empty_values
|
|
||||||
# Convert "empty" values to null
|
# Convert "empty" values to null
|
||||||
self.instance.custom_field_data[key] = value if value not in empty_values else None
|
if value in self.fields[cf_name].empty_values:
|
||||||
|
self.instance.custom_field_data[key] = None
|
||||||
|
else:
|
||||||
|
self.instance.custom_field_data[key] = customfield.serialize(value)
|
||||||
|
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
@ -86,40 +89,37 @@ class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
|||||||
return customfield.to_form_field(for_csv_import=True)
|
return customfield.to_form_field(for_csv_import=True)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldModelBulkEditForm(BulkEditForm):
|
class CustomFieldModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def _get_form_field(self, customfield):
|
||||||
super().__init__(*args, **kwargs)
|
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||||
|
|
||||||
self.custom_fields = []
|
def _append_customfield_fields(self):
|
||||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
"""
|
||||||
|
Append form fields for all CustomFields assigned to this object type.
|
||||||
# Add all applicable CustomFields to the form
|
"""
|
||||||
custom_fields = CustomField.objects.filter(content_types=self.obj_type)
|
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||||
for cf in custom_fields:
|
|
||||||
# Annotate non-required custom fields as nullable
|
# Annotate non-required custom fields as nullable
|
||||||
if not cf.required:
|
if not customfield.required:
|
||||||
self.nullable_fields.append(cf.name)
|
self.nullable_fields.append(customfield.name)
|
||||||
self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
|
|
||||||
# Annotate this as a custom field
|
self.fields[customfield.name] = self._get_form_field(customfield)
|
||||||
self.custom_fields.append(cf.name)
|
|
||||||
|
# Annotate the field in the list of CustomField form fields
|
||||||
|
self.custom_fields[customfield.name] = customfield
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldModelFilterForm(FilterForm):
|
class CustomFieldModelFilterForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Search'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def _get_custom_fields(self, content_type):
|
||||||
|
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
|
||||||
self.custom_field_filters = []
|
|
||||||
custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
|
|
||||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||||
)
|
)
|
||||||
for cf in custom_fields:
|
|
||||||
field_name = f'cf_{cf.name}'
|
def _get_form_field(self, customfield):
|
||||||
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
|
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||||
self.custom_field_filters.append(field_name)
|
|
||||||
|
@ -58,15 +58,18 @@ class CustomFieldFilterForm(FilterForm):
|
|||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(FilterForm):
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q'],
|
['q'],
|
||||||
['content_type', 'weight', 'new_window'],
|
['content_type', 'enabled', 'new_window', 'weight'],
|
||||||
]
|
]
|
||||||
content_type = ContentTypeChoiceField(
|
content_type = ContentTypeChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_fields'),
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
weight = forms.IntegerField(
|
enabled = forms.NullBooleanField(
|
||||||
required=False
|
required=False,
|
||||||
|
widget=StaticSelect(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
new_window = forms.NullBooleanField(
|
new_window = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -74,6 +77,9 @@ class CustomLinkFilterForm(FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateFilterForm(FilterForm):
|
class ExportTemplateFilterForm(FilterForm):
|
||||||
|
@ -7,8 +7,8 @@ from extras.models import *
|
|||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
|
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
|
||||||
ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
|
DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@ -35,12 +35,16 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = CustomField
|
model = CustomField
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')),
|
('Custom Field', ('name', 'label', 'type', 'object_type', 'weight', 'required', 'description')),
|
||||||
('Assigned Models', ('content_types',)),
|
('Assigned Models', ('content_types',)),
|
||||||
('Behavior', ('filter_logic',)),
|
('Behavior', ('filter_logic',)),
|
||||||
('Values', ('default', 'choices')),
|
('Values', ('default', 'choices')),
|
||||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||||
)
|
)
|
||||||
|
widgets = {
|
||||||
|
'type': StaticSelect(),
|
||||||
|
'filter_logic': StaticSelect(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -53,10 +57,11 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
|
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
|
||||||
('Templates', ('link_text', 'link_url')),
|
('Templates', ('link_text', 'link_url')),
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'button_class': StaticSelect(),
|
||||||
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
}
|
}
|
||||||
@ -77,7 +82,7 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Link', ('name', 'content_type', 'description')),
|
('Export Template', ('name', 'content_type', 'description')),
|
||||||
('Template', ('template_code',)),
|
('Template', ('template_code',)),
|
||||||
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
||||||
)
|
)
|
||||||
@ -96,8 +101,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = Webhook
|
model = Webhook
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Webhook', ('name', 'enabled')),
|
('Webhook', ('name', 'content_types', 'enabled')),
|
||||||
('Assigned Models', ('content_types',)),
|
|
||||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||||
('HTTP Request', (
|
('HTTP Request', (
|
||||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||||
@ -105,7 +109,13 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
|||||||
('Conditions', ('conditions',)),
|
('Conditions', ('conditions',)),
|
||||||
('SSL', ('ssl_verification', 'ca_file_path')),
|
('SSL', ('ssl_verification', 'ca_file_path')),
|
||||||
)
|
)
|
||||||
|
labels = {
|
||||||
|
'type_create': 'Creations',
|
||||||
|
'type_update': 'Updates',
|
||||||
|
'type_delete': 'Deletions',
|
||||||
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'http_method': StaticSelect(),
|
||||||
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
}
|
}
|
||||||
|
18
netbox/extras/migrations/0069_custom_object_field.py
Normal file
18
netbox/extras/migrations/0069_custom_object_field.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0068_configcontext_cluster_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='object_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
]
|
18
netbox/extras/migrations/0070_customlink_enabled.py
Normal file
18
netbox/extras/migrations/0070_customlink_enabled.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.11 on 2022-01-10 16:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0069_custom_object_field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='enabled',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
89
netbox/extras/migrations/0071_standardize_id_fields.py
Normal file
89
netbox/extras/migrations/0071_standardize_id_fields.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0070_customlink_enabled'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Model IDs
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='configcontext',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='configrevision',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='imageattachment',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='jobresult',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='journalentry',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectchange',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='taggeditem',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
|
||||||
|
# GFK IDs
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='imageattachment',
|
||||||
|
name='object_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='journalentry',
|
||||||
|
name='assigned_object_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectchange',
|
||||||
|
name='changed_object_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectchange',
|
||||||
|
name='related_object_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -5,11 +5,10 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from netbox.models import BigIDModel
|
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
|
|
||||||
class ObjectChange(BigIDModel):
|
class ObjectChange(models.Model):
|
||||||
"""
|
"""
|
||||||
Record a change to an object and the user account associated with that change. A change record may optionally
|
Record a change to an object and the user account associated with that change. A change record may optionally
|
||||||
indicate an object related to the one being changed. For example, a change to an interface may also indicate the
|
indicate an object related to the one being changed. For example, a change to an interface may also indicate the
|
||||||
@ -43,7 +42,7 @@ class ObjectChange(BigIDModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
changed_object_id = models.PositiveIntegerField()
|
changed_object_id = models.PositiveBigIntegerField()
|
||||||
changed_object = GenericForeignKey(
|
changed_object = GenericForeignKey(
|
||||||
ct_field='changed_object_type',
|
ct_field='changed_object_type',
|
||||||
fk_field='changed_object_id'
|
fk_field='changed_object_id'
|
||||||
@ -55,7 +54,7 @@ class ObjectChange(BigIDModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
related_object_id = models.PositiveIntegerField(
|
related_object_id = models.PositiveBigIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
@ -5,8 +5,8 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from extras.querysets import ConfigContextQuerySet
|
from extras.querysets import ConfigContextQuerySet
|
||||||
from extras.utils import extras_features
|
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
|
from netbox.models.features import WebhooksMixin
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
|
|
||||||
|
|
||||||
@ -20,8 +20,7 @@ __all__ = (
|
|||||||
# Config contexts
|
# Config contexts
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
class ConfigContext(WebhooksMixin, ChangeLoggedModel):
|
||||||
class ConfigContext(ChangeLoggedModel):
|
|
||||||
"""
|
"""
|
||||||
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||||
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||||
|
@ -12,11 +12,13 @@ from django.utils.html import escape
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.utils import FeatureQuery, extras_features
|
from extras.utils import FeatureQuery
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
|
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
|
||||||
from utilities import filters
|
from utilities import filters
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
CSVChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
|
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
|
LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
|
||||||
)
|
)
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.validators import validate_regex
|
from utilities.validators import validate_regex
|
||||||
@ -39,8 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|||||||
return self.get_queryset().filter(content_types=content_type)
|
return self.get_queryset().filter(content_types=content_type)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
class CustomField(ChangeLoggedModel):
|
|
||||||
content_types = models.ManyToManyField(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
related_name='custom_fields',
|
related_name='custom_fields',
|
||||||
@ -50,7 +51,15 @@ class CustomField(ChangeLoggedModel):
|
|||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
default=CustomFieldTypeChoices.TYPE_TEXT
|
default=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
|
help_text='The type of data this custom field holds'
|
||||||
|
)
|
||||||
|
object_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text='The type of NetBox object this field maps to (for object fields)'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -122,7 +131,6 @@ class CustomField(ChangeLoggedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text='Comma-separated list of available choices (for selection fields)'
|
help_text='Comma-separated list of available choices (for selection fields)'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = CustomFieldManager()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -234,11 +242,48 @@ class CustomField(ChangeLoggedModel):
|
|||||||
'default': f"The specified default value ({self.default}) is not listed as an available choice."
|
'default': f"The specified default value ({self.default}) is not listed as an available choice."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Object fields must define an object_type; other fields must not
|
||||||
|
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||||
|
if not self.object_type:
|
||||||
|
raise ValidationError({
|
||||||
|
'object_type': "Object fields must define an object type."
|
||||||
|
})
|
||||||
|
elif self.object_type:
|
||||||
|
raise ValidationError({
|
||||||
|
'object_type': f"{self.get_type_display()} fields may not define an object type."
|
||||||
|
})
|
||||||
|
|
||||||
|
def serialize(self, value):
|
||||||
|
"""
|
||||||
|
Prepare a value for storage as JSON data.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
if self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
return value.pk
|
||||||
|
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
|
return [obj.pk for obj in value] or None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def deserialize(self, value):
|
||||||
|
"""
|
||||||
|
Convert JSON data to a Python object suitable for the field type.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
if self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
model = self.object_type.model_class()
|
||||||
|
return model.objects.filter(pk=value).first()
|
||||||
|
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
|
model = self.object_type.model_class()
|
||||||
|
return model.objects.filter(pk__in=value)
|
||||||
|
return value
|
||||||
|
|
||||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||||
"""
|
"""
|
||||||
Return a form field suitable for setting a CustomField's value for an object.
|
Return a form field suitable for setting a CustomField's value for an object.
|
||||||
|
|
||||||
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
|
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
||||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||||
"""
|
"""
|
||||||
@ -287,7 +332,7 @@ class CustomField(ChangeLoggedModel):
|
|||||||
choices=choices, required=required, initial=initial, widget=StaticSelect()
|
choices=choices, required=required, initial=initial, widget=StaticSelect()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
field_class = CSVChoiceField if for_csv_import else forms.MultipleChoiceField
|
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
|
||||||
field = field_class(
|
field = field_class(
|
||||||
choices=choices, required=required, initial=initial, widget=StaticSelectMultiple()
|
choices=choices, required=required, initial=initial, widget=StaticSelectMultiple()
|
||||||
)
|
)
|
||||||
@ -300,6 +345,24 @@ class CustomField(ChangeLoggedModel):
|
|||||||
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
||||||
field = forms.JSONField(required=required, initial=initial)
|
field = forms.JSONField(required=required, initial=initial)
|
||||||
|
|
||||||
|
# Object
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
model = self.object_type.model_class()
|
||||||
|
field = DynamicModelChoiceField(
|
||||||
|
queryset=model.objects.all(),
|
||||||
|
required=required,
|
||||||
|
initial=initial
|
||||||
|
)
|
||||||
|
|
||||||
|
# Multiple objects
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
|
model = self.object_type.model_class()
|
||||||
|
field = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=model.objects.all(),
|
||||||
|
required=required,
|
||||||
|
initial=initial
|
||||||
|
)
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
else:
|
else:
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT:
|
if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT:
|
||||||
|
@ -17,8 +17,9 @@ from rest_framework.utils.encoders import JSONEncoder
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.constants import *
|
from extras.constants import *
|
||||||
from extras.conditions import ConditionSet
|
from extras.conditions import ConditionSet
|
||||||
from extras.utils import extras_features, FeatureQuery, image_upload
|
from extras.utils import FeatureQuery, image_upload
|
||||||
from netbox.models import BigIDModel, ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
|
from netbox.models.features import ExportTemplatesMixin, JobResultsMixin, WebhooksMixin
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import render_jinja2
|
from utilities.utils import render_jinja2
|
||||||
|
|
||||||
@ -35,8 +36,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
class Webhook(ChangeLoggedModel):
|
|
||||||
"""
|
"""
|
||||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||||
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
||||||
@ -68,7 +68,8 @@ class Webhook(ChangeLoggedModel):
|
|||||||
payload_url = models.CharField(
|
payload_url = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name='URL',
|
verbose_name='URL',
|
||||||
help_text="A POST will be sent to this URL when the webhook is called."
|
help_text='This URL will be called using the HTTP method defined when the webhook is called. '
|
||||||
|
'Jinja2 template processing is supported with the same context as the request body.'
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
default=True
|
default=True
|
||||||
@ -176,9 +177,14 @@ class Webhook(ChangeLoggedModel):
|
|||||||
else:
|
else:
|
||||||
return json.dumps(context, cls=JSONEncoder)
|
return json.dumps(context, cls=JSONEncoder)
|
||||||
|
|
||||||
|
def render_payload_url(self, context):
|
||||||
|
"""
|
||||||
|
Render the payload URL.
|
||||||
|
"""
|
||||||
|
return render_jinja2(self.payload_url, context)
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
|
||||||
class CustomLink(ChangeLoggedModel):
|
class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||||
code to be rendered with an object as context.
|
code to be rendered with an object as context.
|
||||||
@ -192,6 +198,9 @@ class CustomLink(ChangeLoggedModel):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
|
enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
link_text = models.CharField(
|
link_text = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
help_text="Jinja2 template code for link text"
|
help_text="Jinja2 template code for link text"
|
||||||
@ -248,8 +257,7 @@ class CustomLink(ChangeLoggedModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
class ExportTemplate(ChangeLoggedModel):
|
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -335,8 +343,7 @@ class ExportTemplate(ChangeLoggedModel):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
||||||
class ImageAttachment(ChangeLoggedModel):
|
|
||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
"""
|
"""
|
||||||
@ -344,7 +351,7 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveBigIntegerField()
|
||||||
parent = GenericForeignKey(
|
parent = GenericForeignKey(
|
||||||
ct_field='content_type',
|
ct_field='content_type',
|
||||||
fk_field='object_id'
|
fk_field='object_id'
|
||||||
@ -411,11 +418,12 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
return super().to_objectchange(action, related_object=self.parent)
|
objectchange = super().to_objectchange(action)
|
||||||
|
objectchange.related_object = self.parent
|
||||||
|
return objectchange
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
class JournalEntry(WebhooksMixin, ChangeLoggedModel):
|
||||||
class JournalEntry(ChangeLoggedModel):
|
|
||||||
"""
|
"""
|
||||||
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
||||||
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
||||||
@ -425,7 +433,7 @@ class JournalEntry(ChangeLoggedModel):
|
|||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
assigned_object_id = models.PositiveIntegerField()
|
assigned_object_id = models.PositiveBigIntegerField()
|
||||||
assigned_object = GenericForeignKey(
|
assigned_object = GenericForeignKey(
|
||||||
ct_field='assigned_object_type',
|
ct_field='assigned_object_type',
|
||||||
fk_field='assigned_object_id'
|
fk_field='assigned_object_id'
|
||||||
@ -461,7 +469,7 @@ class JournalEntry(ChangeLoggedModel):
|
|||||||
return JournalEntryKindChoices.colors.get(self.kind)
|
return JournalEntryKindChoices.colors.get(self.kind)
|
||||||
|
|
||||||
|
|
||||||
class JobResult(BigIDModel):
|
class JobResult(models.Model):
|
||||||
"""
|
"""
|
||||||
This model stores the results from running a user-defined report.
|
This model stores the results from running a user-defined report.
|
||||||
"""
|
"""
|
||||||
@ -593,8 +601,7 @@ class ConfigRevision(models.Model):
|
|||||||
# Custom scripts & reports
|
# Custom scripts & reports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('job_results')
|
class Script(JobResultsMixin, models.Model):
|
||||||
class Script(models.Model):
|
|
||||||
"""
|
"""
|
||||||
Dummy model used to generate permissions for custom scripts. Does not exist in the database.
|
Dummy model used to generate permissions for custom scripts. Does not exist in the database.
|
||||||
"""
|
"""
|
||||||
@ -606,8 +613,7 @@ class Script(models.Model):
|
|||||||
# Reports
|
# Reports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('job_results')
|
class Report(JobResultsMixin, models.Model):
|
||||||
class Report(models.Model):
|
|
||||||
"""
|
"""
|
||||||
Dummy model used to generate permissions for reports. Does not exist in the database.
|
Dummy model used to generate permissions for reports. Does not exist in the database.
|
||||||
"""
|
"""
|
||||||
|
@ -3,8 +3,8 @@ from django.urls import reverse
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from taggit.models import TagBase, GenericTaggedItemBase
|
from taggit.models import TagBase, GenericTaggedItemBase
|
||||||
|
|
||||||
from extras.utils import extras_features
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models import BigIDModel, ChangeLoggedModel
|
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
|
|
||||||
@ -13,8 +13,10 @@ from utilities.fields import ColorField
|
|||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
class Tag(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase):
|
||||||
class Tag(ChangeLoggedModel, TagBase):
|
id = models.BigAutoField(
|
||||||
|
primary_key=True
|
||||||
|
)
|
||||||
color = ColorField(
|
color = ColorField(
|
||||||
default=ColorChoices.COLOR_GREY
|
default=ColorChoices.COLOR_GREY
|
||||||
)
|
)
|
||||||
@ -37,7 +39,7 @@ class Tag(ChangeLoggedModel, TagBase):
|
|||||||
return slug
|
return slug
|
||||||
|
|
||||||
|
|
||||||
class TaggedItem(BigIDModel, GenericTaggedItemBase):
|
class TaggedItem(GenericTaggedItemBase):
|
||||||
tag = models.ForeignKey(
|
tag = models.ForeignKey(
|
||||||
to=Tag,
|
to=Tag,
|
||||||
related_name="%(app_label)s_%(class)s_items",
|
related_name="%(app_label)s_%(class)s_items",
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
from extras.constants import EXTRAS_FEATURES
|
||||||
|
|
||||||
|
|
||||||
class Registry(dict):
|
class Registry(dict):
|
||||||
"""
|
"""
|
||||||
Central registry for registration of functionality. Once a store (key) is defined, it cannot be overwritten or
|
Central registry for registration of functionality. Once a store (key) is defined, it cannot be overwritten or
|
||||||
@ -7,15 +12,19 @@ class Registry(dict):
|
|||||||
try:
|
try:
|
||||||
return super().__getitem__(key)
|
return super().__getitem__(key)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError("Invalid store: {}".format(key))
|
raise KeyError(f"Invalid store: {key}")
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
if key in self:
|
if key in self:
|
||||||
raise KeyError("Store already set: {}".format(key))
|
raise KeyError(f"Store already set: {key}")
|
||||||
super().__setitem__(key, value)
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
raise TypeError("Cannot delete stores from registry")
|
raise TypeError("Cannot delete stores from registry")
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize the global registry
|
||||||
registry = Registry()
|
registry = Registry()
|
||||||
|
registry['model_features'] = {
|
||||||
|
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ from extras.models import JobResult
|
|||||||
from ipam.formfields import IPAddressFormField, IPNetworkFormField
|
from ipam.formfields import IPAddressFormField, IPNetworkFormField
|
||||||
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
|
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
|
||||||
from utilities.exceptions import AbortTransaction
|
from utilities.exceptions import AbortTransaction
|
||||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from .context_managers import change_logging
|
from .context_managers import change_logging
|
||||||
from .forms import ScriptForm
|
from .forms import ScriptForm
|
||||||
|
|
||||||
@ -164,16 +164,22 @@ class ChoiceVar(ScriptVariable):
|
|||||||
def __init__(self, choices, *args, **kwargs):
|
def __init__(self, choices, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Set field choices
|
# Set field choices, adding a blank choice to avoid forced selections
|
||||||
self.field_attrs['choices'] = choices
|
self.field_attrs['choices'] = add_blank_choice(choices)
|
||||||
|
|
||||||
|
|
||||||
class MultiChoiceVar(ChoiceVar):
|
class MultiChoiceVar(ScriptVariable):
|
||||||
"""
|
"""
|
||||||
Like ChoiceVar, but allows for the selection of multiple choices.
|
Like ChoiceVar, but allows for the selection of multiple choices.
|
||||||
"""
|
"""
|
||||||
form_field = forms.MultipleChoiceField
|
form_field = forms.MultipleChoiceField
|
||||||
|
|
||||||
|
def __init__(self, choices, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Set field choices
|
||||||
|
self.field_attrs['choices'] = choices
|
||||||
|
|
||||||
|
|
||||||
class ObjectVar(ScriptVariable):
|
class ObjectVar(ScriptVariable):
|
||||||
"""
|
"""
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from utilities.tables import (
|
from netbox.tables import NetBoxTable, columns
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn,
|
|
||||||
MarkdownColumn, ToggleColumn,
|
|
||||||
)
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -46,19 +43,18 @@ OBJECTCHANGE_REQUEST_ID = """
|
|||||||
# Custom fields
|
# Custom fields
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFieldTable(BaseTable):
|
class CustomFieldTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = ContentTypesColumn()
|
content_types = columns.ContentTypesColumn()
|
||||||
required = BooleanColumn()
|
required = columns.BooleanColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
|
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
|
||||||
'description', 'filter_logic', 'choices',
|
'description', 'filter_logic', 'choices', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
|
||||||
|
|
||||||
@ -67,39 +63,39 @@ class CustomFieldTable(BaseTable):
|
|||||||
# Custom links
|
# Custom links
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomLinkTable(BaseTable):
|
class CustomLinkTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_type = ContentTypeColumn()
|
content_type = columns.ContentTypeColumn()
|
||||||
new_window = BooleanColumn()
|
enabled = columns.BooleanColumn()
|
||||||
|
new_window = columns.BooleanColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name',
|
'pk', 'id', 'name', 'content_type', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||||
'button_class', 'new_window',
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
|
default_columns = ('pk', 'name', 'content_type', 'enabled', 'group_name', 'button_class', 'new_window')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplateTable(BaseTable):
|
class ExportTemplateTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_type = ContentTypeColumn()
|
content_type = columns.ContentTypeColumn()
|
||||||
as_attachment = BooleanColumn()
|
as_attachment = columns.BooleanColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
@ -110,31 +106,30 @@ class ExportTemplateTable(BaseTable):
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
|
||||||
class WebhookTable(BaseTable):
|
class WebhookTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = ContentTypesColumn()
|
content_types = columns.ContentTypesColumn()
|
||||||
enabled = BooleanColumn()
|
enabled = columns.BooleanColumn()
|
||||||
type_create = BooleanColumn(
|
type_create = columns.BooleanColumn(
|
||||||
verbose_name='Create'
|
verbose_name='Create'
|
||||||
)
|
)
|
||||||
type_update = BooleanColumn(
|
type_update = columns.BooleanColumn(
|
||||||
verbose_name='Update'
|
verbose_name='Update'
|
||||||
)
|
)
|
||||||
type_delete = BooleanColumn(
|
type_delete = columns.BooleanColumn(
|
||||||
verbose_name='Delete'
|
verbose_name='Delete'
|
||||||
)
|
)
|
||||||
ssl_validation = BooleanColumn(
|
ssl_validation = columns.BooleanColumn(
|
||||||
verbose_name='SSL Validation'
|
verbose_name='SSL Validation'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Webhook
|
model = Webhook
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||||
'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
|
'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||||
@ -146,27 +141,25 @@ class WebhookTable(BaseTable):
|
|||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
|
||||||
class TagTable(BaseTable):
|
class TagTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = columns.ColorColumn()
|
||||||
actions = ButtonsColumn(Tag)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'created', 'last_updated', 'actions')
|
||||||
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
class TaggedItemTable(BaseTable):
|
class TaggedItemTable(NetBoxTable):
|
||||||
id = tables.Column(
|
id = tables.Column(
|
||||||
verbose_name='ID',
|
verbose_name='ID',
|
||||||
linkify=lambda record: record.content_object.get_absolute_url(),
|
linkify=lambda record: record.content_object.get_absolute_url(),
|
||||||
accessor='content_object__id'
|
accessor='content_object__id'
|
||||||
)
|
)
|
||||||
content_type = ContentTypeColumn(
|
content_type = columns.ContentTypeColumn(
|
||||||
verbose_name='Type'
|
verbose_name='Type'
|
||||||
)
|
)
|
||||||
content_object = tables.Column(
|
content_object = tables.Column(
|
||||||
@ -175,36 +168,36 @@ class TaggedItemTable(BaseTable):
|
|||||||
verbose_name='Object'
|
verbose_name='Object'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = TaggedItem
|
model = TaggedItem
|
||||||
fields = ('id', 'content_type', 'content_object')
|
fields = ('id', 'content_type', 'content_object')
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextTable(BaseTable):
|
class ConfigContextTable(NetBoxTable):
|
||||||
pk = ToggleColumn()
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
is_active = BooleanColumn(
|
is_active = columns.BooleanColumn(
|
||||||
verbose_name='Active'
|
verbose_name='Active'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
|
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
|
||||||
'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created',
|
||||||
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
|
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeTable(BaseTable):
|
class ObjectChangeTable(NetBoxTable):
|
||||||
time = tables.DateTimeColumn(
|
time = tables.DateTimeColumn(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
format=settings.SHORT_DATETIME_FORMAT
|
format=settings.SHORT_DATETIME_FORMAT
|
||||||
)
|
)
|
||||||
action = ChoiceFieldColumn()
|
action = columns.ChoiceFieldColumn()
|
||||||
changed_object_type = ContentTypeColumn(
|
changed_object_type = columns.ContentTypeColumn(
|
||||||
verbose_name='Type'
|
verbose_name='Type'
|
||||||
)
|
)
|
||||||
object_repr = tables.TemplateColumn(
|
object_repr = tables.TemplateColumn(
|
||||||
@ -215,13 +208,14 @@ class ObjectChangeTable(BaseTable):
|
|||||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||||
verbose_name='Request ID'
|
verbose_name='Request ID'
|
||||||
)
|
)
|
||||||
|
actions = columns.ActionsColumn(sequence=())
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||||
|
|
||||||
|
|
||||||
class ObjectJournalTable(BaseTable):
|
class ObjectJournalTable(NetBoxTable):
|
||||||
"""
|
"""
|
||||||
Used for displaying a set of JournalEntries within the context of a single object.
|
Used for displaying a set of JournalEntries within the context of a single object.
|
||||||
"""
|
"""
|
||||||
@ -229,22 +223,18 @@ class ObjectJournalTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
format=settings.SHORT_DATETIME_FORMAT
|
format=settings.SHORT_DATETIME_FORMAT
|
||||||
)
|
)
|
||||||
kind = ChoiceFieldColumn()
|
kind = columns.ChoiceFieldColumn()
|
||||||
comments = tables.TemplateColumn(
|
comments = tables.TemplateColumn(
|
||||||
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}'
|
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}'
|
||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
|
||||||
model=JournalEntry
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
|
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryTable(ObjectJournalTable):
|
class JournalEntryTable(ObjectJournalTable):
|
||||||
pk = ToggleColumn()
|
assigned_object_type = columns.ContentTypeColumn(
|
||||||
assigned_object_type = ContentTypeColumn(
|
|
||||||
verbose_name='Object type'
|
verbose_name='Object type'
|
||||||
)
|
)
|
||||||
assigned_object = tables.Column(
|
assigned_object = tables.Column(
|
||||||
@ -252,15 +242,14 @@ class JournalEntryTable(ObjectJournalTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Object'
|
verbose_name='Object'
|
||||||
)
|
)
|
||||||
comments = MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
||||||
'comments', 'actions'
|
'comments', 'actions'
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
||||||
'comments', 'actions'
|
|
||||||
)
|
)
|
||||||
|
@ -36,7 +36,7 @@ def custom_links(context, obj):
|
|||||||
Render all applicable links for the given object.
|
Render all applicable links for the given object.
|
||||||
"""
|
"""
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
custom_links = CustomLink.objects.filter(content_type=content_type)
|
custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
|
||||||
if not custom_links:
|
if not custom_links:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -139,24 +139,28 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_type': 'dcim.site',
|
||||||
'name': 'Custom Link 4',
|
'name': 'Custom Link 4',
|
||||||
|
'enabled': True,
|
||||||
'link_text': 'Link 4',
|
'link_text': 'Link 4',
|
||||||
'link_url': 'http://example.com/?4',
|
'link_url': 'http://example.com/?4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_type': 'dcim.site',
|
||||||
'name': 'Custom Link 5',
|
'name': 'Custom Link 5',
|
||||||
|
'enabled': True,
|
||||||
'link_text': 'Link 5',
|
'link_text': 'Link 5',
|
||||||
'link_url': 'http://example.com/?5',
|
'link_url': 'http://example.com/?5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_type': 'dcim.site',
|
||||||
'name': 'Custom Link 6',
|
'name': 'Custom Link 6',
|
||||||
|
'enabled': False,
|
||||||
'link_text': 'Link 6',
|
'link_text': 'Link 6',
|
||||||
'link_url': 'http://example.com/?6',
|
'link_url': 'http://example.com/?6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'new_window': True,
|
'new_window': True,
|
||||||
|
'enabled': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -167,18 +171,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
content_type=site_ct,
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
|
enabled=True,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
link_url='http://example.com/?1',
|
link_url='http://example.com/?1',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
content_type=site_ct,
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
|
enabled=True,
|
||||||
link_text='Link 2',
|
link_text='Link 2',
|
||||||
link_url='http://example.com/?2',
|
link_url='http://example.com/?2',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
content_type=site_ct,
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
|
enabled=False,
|
||||||
link_text='Link 3',
|
link_text='Link 3',
|
||||||
link_url='http://example.com/?3',
|
link_url='http://example.com/?3',
|
||||||
),
|
),
|
||||||
|
@ -8,13 +8,15 @@ from dcim.forms import SiteCSVForm
|
|||||||
from dcim.models import Site, Rack
|
from dcim.models import Site, Rack
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
|
from ipam.models import VLAN
|
||||||
from utilities.testing import APITestCase, TestCase
|
from utilities.testing import APITestCase, TestCase
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldTest(TestCase):
|
class CustomFieldTest(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
Site.objects.bulk_create([
|
Site.objects.bulk_create([
|
||||||
Site(name='Site A', slug='site-a'),
|
Site(name='Site A', slug='site-a'),
|
||||||
@ -22,137 +24,294 @@ class CustomFieldTest(TestCase):
|
|||||||
Site(name='Site C', slug='site-c'),
|
Site(name='Site C', slug='site-c'),
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_simple_fields(self):
|
cls.object_type = ContentType.objects.get_for_model(Site)
|
||||||
DATA = (
|
|
||||||
{
|
def test_text_field(self):
|
||||||
'field': {
|
value = 'Foobar!'
|
||||||
'type': CustomFieldTypeChoices.TYPE_TEXT,
|
|
||||||
},
|
# Create a custom field & check that initial value is null
|
||||||
'value': 'Foobar!',
|
cf = CustomField.objects.create(
|
||||||
},
|
name='text_field',
|
||||||
{
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
'field': {
|
required=False
|
||||||
'type': CustomFieldTypeChoices.TYPE_LONGTEXT,
|
|
||||||
},
|
|
||||||
'value': 'Text with **Markdown**',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_INTEGER,
|
|
||||||
},
|
|
||||||
'value': 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_INTEGER,
|
|
||||||
'validation_minimum': 1,
|
|
||||||
'validation_maximum': 100,
|
|
||||||
},
|
|
||||||
'value': 42,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_INTEGER,
|
|
||||||
'validation_minimum': -100,
|
|
||||||
'validation_maximum': -1,
|
|
||||||
},
|
|
||||||
'value': -42,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
|
|
||||||
},
|
|
||||||
'value': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
|
|
||||||
},
|
|
||||||
'value': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_DATE,
|
|
||||||
},
|
|
||||||
'value': '2016-06-23',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_URL,
|
|
||||||
},
|
|
||||||
'value': 'http://example.com/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'field': {
|
|
||||||
'type': CustomFieldTypeChoices.TYPE_JSON,
|
|
||||||
},
|
|
||||||
'value': '{"foo": 1, "bar": 2}',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
for data in DATA:
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
# Create a custom field
|
def test_longtext_field(self):
|
||||||
cf = CustomField(name='my_field', required=False, **data['field'])
|
value = 'A' * 256
|
||||||
cf.save()
|
|
||||||
cf.content_types.set([obj_type])
|
|
||||||
|
|
||||||
# Check that the field has a null initial value
|
# Create a custom field & check that initial value is null
|
||||||
site = Site.objects.first()
|
cf = CustomField.objects.create(
|
||||||
self.assertIsNone(site.custom_field_data[cf.name])
|
name='longtext_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_LONGTEXT,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
# Assign a value to the first Site
|
# Assign a value and check that it is saved
|
||||||
site.custom_field_data[cf.name] = data['value']
|
instance.custom_field_data[cf.name] = value
|
||||||
site.save()
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
# Retrieve the stored value
|
# Delete the stored value and check that it is now null
|
||||||
site.refresh_from_db()
|
instance.custom_field_data.pop(cf.name)
|
||||||
self.assertEqual(site.custom_field_data[cf.name], data['value'])
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
# Delete the stored value
|
def test_integer_field(self):
|
||||||
site.custom_field_data.pop(cf.name)
|
|
||||||
site.save()
|
|
||||||
site.refresh_from_db()
|
|
||||||
self.assertIsNone(site.custom_field_data.get(cf.name))
|
|
||||||
|
|
||||||
# Delete the custom field
|
# Create a custom field & check that initial value is null
|
||||||
cf.delete()
|
cf = CustomField.objects.create(
|
||||||
|
name='integer_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
for value in (123456, 0, -123456):
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_boolean_field(self):
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='boolean_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
for value in (True, False):
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_date_field(self):
|
||||||
|
value = '2016-06-23'
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='date_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_url_field(self):
|
||||||
|
value = 'http://example.com/'
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='url_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_URL,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_json_field(self):
|
||||||
|
value = '{"foo": 1, "bar": 2}'
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='json_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_JSON,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
def test_select_field(self):
|
def test_select_field(self):
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
CHOICES = ('Option A', 'Option B', 'Option C')
|
||||||
|
value = CHOICES[1]
|
||||||
|
|
||||||
# Create a custom field
|
# Create a custom field & check that initial value is null
|
||||||
cf = CustomField(
|
cf = CustomField.objects.create(
|
||||||
|
name='select_field',
|
||||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
name='my_field',
|
|
||||||
required=False,
|
required=False,
|
||||||
choices=['Option A', 'Option B', 'Option C']
|
choices=CHOICES
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.content_types.set([self.object_type])
|
||||||
cf.content_types.set([obj_type])
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
# Check that the field has a null initial value
|
# Assign a value and check that it is saved
|
||||||
site = Site.objects.first()
|
instance.custom_field_data[cf.name] = value
|
||||||
self.assertIsNone(site.custom_field_data[cf.name])
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
# Assign a value to the first Site
|
# Delete the stored value and check that it is now null
|
||||||
site.custom_field_data[cf.name] = 'Option A'
|
instance.custom_field_data.pop(cf.name)
|
||||||
site.save()
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
# Retrieve the stored value
|
def test_multiselect_field(self):
|
||||||
site.refresh_from_db()
|
CHOICES = ['Option A', 'Option B', 'Option C']
|
||||||
self.assertEqual(site.custom_field_data[cf.name], 'Option A')
|
value = [CHOICES[1], CHOICES[2]]
|
||||||
|
|
||||||
# Delete the stored value
|
# Create a custom field & check that initial value is null
|
||||||
site.custom_field_data.pop(cf.name)
|
cf = CustomField.objects.create(
|
||||||
site.save()
|
name='multiselect_field',
|
||||||
site.refresh_from_db()
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||||
self.assertIsNone(site.custom_field_data.get(cf.name))
|
required=False,
|
||||||
|
choices=CHOICES
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
# Delete the custom field
|
# Assign a value and check that it is saved
|
||||||
cf.delete()
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_object_field(self):
|
||||||
|
value = VLAN.objects.create(name='VLAN 1', vid=1).pk
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='object_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_multiobject_field(self):
|
||||||
|
vlans = (
|
||||||
|
VLAN(name='VLAN 1', vid=1),
|
||||||
|
VLAN(name='VLAN 2', vid=2),
|
||||||
|
VLAN(name='VLAN 3', vid=3),
|
||||||
|
)
|
||||||
|
VLAN.objects.bulk_create(vlans)
|
||||||
|
value = [vlan.pk for vlan in vlans]
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='object_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
def test_rename_customfield(self):
|
def test_rename_customfield(self):
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
obj_type = ContentType.objects.get_for_model(Site)
|
||||||
@ -201,76 +360,116 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_type = ContentType.objects.get_for_model(Site)
|
content_type = ContentType.objects.get_for_model(Site)
|
||||||
|
|
||||||
# Text custom field
|
# Create some VLANs
|
||||||
cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
|
vlans = (
|
||||||
cls.cf_text.save()
|
VLAN(name='VLAN 1', vid=1),
|
||||||
cls.cf_text.content_types.set([content_type])
|
VLAN(name='VLAN 2', vid=2),
|
||||||
|
VLAN(name='VLAN 3', vid=3),
|
||||||
|
VLAN(name='VLAN 4', vid=4),
|
||||||
|
VLAN(name='VLAN 5', vid=5),
|
||||||
|
)
|
||||||
|
VLAN.objects.bulk_create(vlans)
|
||||||
|
|
||||||
# Long text custom field
|
custom_fields = (
|
||||||
cls.cf_longtext = CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC')
|
CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'),
|
||||||
cls.cf_longtext.save()
|
CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC'),
|
||||||
cls.cf_longtext.content_types.set([content_type])
|
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}'),
|
||||||
|
CustomField(
|
||||||
|
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
|
name='select_field',
|
||||||
|
default='Foo',
|
||||||
|
choices=(
|
||||||
|
'Foo', 'Bar', 'Baz'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CustomField(
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||||
|
name='multiselect_field',
|
||||||
|
default=['Foo'],
|
||||||
|
choices=(
|
||||||
|
'Foo', 'Bar', 'Baz'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CustomField(
|
||||||
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
|
name='object_field',
|
||||||
|
object_type=ContentType.objects.get_for_model(VLAN),
|
||||||
|
default=vlans[0].pk,
|
||||||
|
),
|
||||||
|
CustomField(
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
|
name='multiobject_field',
|
||||||
|
object_type=ContentType.objects.get_for_model(VLAN),
|
||||||
|
default=[vlans[0].pk, vlans[1].pk],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for cf in custom_fields:
|
||||||
|
cf.save()
|
||||||
|
cf.content_types.set([content_type])
|
||||||
|
|
||||||
# Integer custom field
|
# Create some sites *after* creating the custom fields. This ensures that
|
||||||
cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123)
|
# default values are not set for the assigned objects.
|
||||||
cls.cf_integer.save()
|
sites = (
|
||||||
cls.cf_integer.content_types.set([content_type])
|
|
||||||
|
|
||||||
# Boolean custom field
|
|
||||||
cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False)
|
|
||||||
cls.cf_boolean.save()
|
|
||||||
cls.cf_boolean.content_types.set([content_type])
|
|
||||||
|
|
||||||
# Date custom field
|
|
||||||
cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01')
|
|
||||||
cls.cf_date.save()
|
|
||||||
cls.cf_date.content_types.set([content_type])
|
|
||||||
|
|
||||||
# URL custom field
|
|
||||||
cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1')
|
|
||||||
cls.cf_url.save()
|
|
||||||
cls.cf_url.content_types.set([content_type])
|
|
||||||
|
|
||||||
# JSON custom field
|
|
||||||
cls.cf_json = CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}')
|
|
||||||
cls.cf_json.save()
|
|
||||||
cls.cf_json.content_types.set([content_type])
|
|
||||||
|
|
||||||
# Select custom field
|
|
||||||
cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field', choices=['Foo', 'Bar', 'Baz'])
|
|
||||||
cls.cf_select.default = 'Foo'
|
|
||||||
cls.cf_select.save()
|
|
||||||
cls.cf_select.content_types.set([content_type])
|
|
||||||
|
|
||||||
# Create some sites
|
|
||||||
cls.sites = (
|
|
||||||
Site(name='Site 1', slug='site-1'),
|
Site(name='Site 1', slug='site-1'),
|
||||||
Site(name='Site 2', slug='site-2'),
|
Site(name='Site 2', slug='site-2'),
|
||||||
)
|
)
|
||||||
Site.objects.bulk_create(cls.sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
# Assign custom field values for site 2
|
# Assign custom field values for site 2
|
||||||
cls.sites[1].custom_field_data = {
|
sites[1].custom_field_data = {
|
||||||
cls.cf_text.name: 'bar',
|
custom_fields[0].name: 'bar',
|
||||||
cls.cf_longtext.name: 'DEF',
|
custom_fields[1].name: 'DEF',
|
||||||
cls.cf_integer.name: 456,
|
custom_fields[2].name: 456,
|
||||||
cls.cf_boolean.name: True,
|
custom_fields[3].name: True,
|
||||||
cls.cf_date.name: '2020-01-02',
|
custom_fields[4].name: '2020-01-02',
|
||||||
cls.cf_url.name: 'http://example.com/2',
|
custom_fields[5].name: 'http://example.com/2',
|
||||||
cls.cf_json.name: '{"foo": 1, "bar": 2}',
|
custom_fields[6].name: '{"foo": 1, "bar": 2}',
|
||||||
cls.cf_select.name: 'Bar',
|
custom_fields[7].name: 'Bar',
|
||||||
|
custom_fields[8].name: ['Bar', 'Baz'],
|
||||||
|
custom_fields[9].name: vlans[1].pk,
|
||||||
|
custom_fields[10].name: [vlans[2].pk, vlans[3].pk],
|
||||||
}
|
}
|
||||||
cls.sites[1].save()
|
sites[1].save()
|
||||||
|
|
||||||
|
def test_get_custom_fields(self):
|
||||||
|
TYPES = {
|
||||||
|
CustomFieldTypeChoices.TYPE_TEXT: 'string',
|
||||||
|
CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
|
||||||
|
CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
|
||||||
|
CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
|
||||||
|
CustomFieldTypeChoices.TYPE_DATE: 'string',
|
||||||
|
CustomFieldTypeChoices.TYPE_URL: 'string',
|
||||||
|
CustomFieldTypeChoices.TYPE_JSON: 'object',
|
||||||
|
CustomFieldTypeChoices.TYPE_SELECT: 'string',
|
||||||
|
CustomFieldTypeChoices.TYPE_MULTISELECT: 'array',
|
||||||
|
CustomFieldTypeChoices.TYPE_OBJECT: 'object',
|
||||||
|
CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_permissions('extras.view_customfield')
|
||||||
|
url = reverse('extras-api:customfield-list')
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
self.assertEqual(response.data['count'], len(TYPES))
|
||||||
|
|
||||||
|
# Validate data types
|
||||||
|
for customfield in response.data['results']:
|
||||||
|
cf_type = customfield['type']['value']
|
||||||
|
self.assertEqual(customfield['data_type'], TYPES[cf_type])
|
||||||
|
|
||||||
def test_get_single_object_without_custom_field_data(self):
|
def test_get_single_object_without_custom_field_data(self):
|
||||||
"""
|
"""
|
||||||
Validate that custom fields are present on an object even if it has no values defined.
|
Validate that custom fields are present on an object even if it has no values defined.
|
||||||
"""
|
"""
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[0].pk})
|
site1 = Site.objects.get(name='Site 1')
|
||||||
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk})
|
||||||
self.add_permissions('dcim.view_site')
|
self.add_permissions('dcim.view_site')
|
||||||
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.data['name'], self.sites[0].name)
|
self.assertEqual(response.data['name'], site1.name)
|
||||||
self.assertEqual(response.data['custom_fields'], {
|
self.assertEqual(response.data['custom_fields'], {
|
||||||
'text_field': None,
|
'text_field': None,
|
||||||
'longtext_field': None,
|
'longtext_field': None,
|
||||||
@ -279,19 +478,23 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'date_field': None,
|
'date_field': None,
|
||||||
'url_field': None,
|
'url_field': None,
|
||||||
'json_field': None,
|
'json_field': None,
|
||||||
'choice_field': None,
|
'select_field': None,
|
||||||
|
'multiselect_field': None,
|
||||||
|
'object_field': None,
|
||||||
|
'multiobject_field': None,
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_get_single_object_with_custom_field_data(self):
|
def test_get_single_object_with_custom_field_data(self):
|
||||||
"""
|
"""
|
||||||
Validate that custom fields are present and correctly set for an object with values defined.
|
Validate that custom fields are present and correctly set for an object with values defined.
|
||||||
"""
|
"""
|
||||||
site2_cfvs = self.sites[1].custom_field_data
|
site2 = Site.objects.get(name='Site 2')
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
site2_cfvs = site2.custom_field_data
|
||||||
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
self.add_permissions('dcim.view_site')
|
self.add_permissions('dcim.view_site')
|
||||||
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.data['name'], self.sites[1].name)
|
self.assertEqual(response.data['name'], site2.name)
|
||||||
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
|
self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
|
self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
|
||||||
@ -299,12 +502,21 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field'])
|
self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['choice_field'], site2_cfvs['choice_field'])
|
self.assertEqual(response.data['custom_fields']['select_field'], site2_cfvs['select_field'])
|
||||||
|
self.assertEqual(response.data['custom_fields']['multiselect_field'], site2_cfvs['multiselect_field'])
|
||||||
|
self.assertEqual(response.data['custom_fields']['object_field']['id'], site2_cfvs['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
|
||||||
|
site2_cfvs['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_single_object_with_defaults(self):
|
def test_create_single_object_with_defaults(self):
|
||||||
"""
|
"""
|
||||||
Create a new site with no specified custom field values and check that it received the default values.
|
Create a new site with no specified custom field values and check that it received the default values.
|
||||||
"""
|
"""
|
||||||
|
cf_defaults = {
|
||||||
|
cf.name: cf.default for cf in CustomField.objects.all()
|
||||||
|
}
|
||||||
data = {
|
data = {
|
||||||
'name': 'Site 3',
|
'name': 'Site 3',
|
||||||
'slug': 'site-3',
|
'slug': 'site-3',
|
||||||
@ -317,25 +529,34 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
|
|
||||||
# Validate response data
|
# Validate response data
|
||||||
response_cf = response.data['custom_fields']
|
response_cf = response.data['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], self.cf_text.default)
|
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], self.cf_longtext.default)
|
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], self.cf_integer.default)
|
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
|
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], self.cf_date.default)
|
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], self.cf_url.default)
|
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||||
self.assertEqual(response_cf['json_field'], self.cf_json.default)
|
self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
|
||||||
self.assertEqual(response_cf['choice_field'], self.cf_select.default)
|
self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
|
||||||
|
self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
|
self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
|
||||||
|
cf_defaults['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data['id'])
|
site = Site.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
|
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], self.cf_longtext.default)
|
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
|
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
|
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
|
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
|
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||||
self.assertEqual(site.custom_field_data['json_field'], self.cf_json.default)
|
self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
|
||||||
self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
|
self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
|
||||||
|
|
||||||
def test_create_single_object_with_values(self):
|
def test_create_single_object_with_values(self):
|
||||||
"""
|
"""
|
||||||
@ -352,7 +573,10 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'date_field': '2020-01-02',
|
'date_field': '2020-01-02',
|
||||||
'url_field': 'http://example.com/2',
|
'url_field': 'http://example.com/2',
|
||||||
'json_field': '{"foo": 1, "bar": 2}',
|
'json_field': '{"foo": 1, "bar": 2}',
|
||||||
'choice_field': 'Bar',
|
'select_field': 'Bar',
|
||||||
|
'multiselect_field': ['Bar', 'Baz'],
|
||||||
|
'object_field': VLAN.objects.get(vid=2).pk,
|
||||||
|
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
url = reverse('dcim-api:site-list')
|
url = reverse('dcim-api:site-list')
|
||||||
@ -371,7 +595,13 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
||||||
self.assertEqual(response_cf['json_field'], data_cf['json_field'])
|
self.assertEqual(response_cf['json_field'], data_cf['json_field'])
|
||||||
self.assertEqual(response_cf['choice_field'], data_cf['choice_field'])
|
self.assertEqual(response_cf['select_field'], data_cf['select_field'])
|
||||||
|
self.assertEqual(response_cf['multiselect_field'], data_cf['multiselect_field'])
|
||||||
|
self.assertEqual(response_cf['object_field']['id'], data_cf['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
|
data_cf['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data['id'])
|
site = Site.objects.get(pk=response.data['id'])
|
||||||
@ -382,13 +612,19 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
|
||||||
self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field'])
|
self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field'])
|
||||||
self.assertEqual(site.custom_field_data['choice_field'], data_cf['choice_field'])
|
self.assertEqual(site.custom_field_data['select_field'], data_cf['select_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiselect_field'], data_cf['multiselect_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['object_field'], data_cf['object_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiobject_field'], data_cf['multiobject_field'])
|
||||||
|
|
||||||
def test_create_multiple_objects_with_defaults(self):
|
def test_create_multiple_objects_with_defaults(self):
|
||||||
"""
|
"""
|
||||||
Create three news sites with no specified custom field values and check that each received
|
Create three new sites with no specified custom field values and check that each received
|
||||||
the default custom field values.
|
the default custom field values.
|
||||||
"""
|
"""
|
||||||
|
cf_defaults = {
|
||||||
|
cf.name: cf.default for cf in CustomField.objects.all()
|
||||||
|
}
|
||||||
data = (
|
data = (
|
||||||
{
|
{
|
||||||
'name': 'Site 3',
|
'name': 'Site 3',
|
||||||
@ -414,25 +650,34 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
|
|
||||||
# Validate response data
|
# Validate response data
|
||||||
response_cf = response.data[i]['custom_fields']
|
response_cf = response.data[i]['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], self.cf_text.default)
|
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], self.cf_longtext.default)
|
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], self.cf_integer.default)
|
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
|
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], self.cf_date.default)
|
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], self.cf_url.default)
|
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||||
self.assertEqual(response_cf['json_field'], self.cf_json.default)
|
self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
|
||||||
self.assertEqual(response_cf['choice_field'], self.cf_select.default)
|
self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
|
||||||
|
self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
|
self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
|
cf_defaults['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
site = Site.objects.get(pk=response.data[i]['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
|
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], self.cf_longtext.default)
|
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
|
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
|
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
|
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
|
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||||
self.assertEqual(site.custom_field_data['json_field'], self.cf_json.default)
|
self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
|
||||||
self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
|
self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
|
||||||
|
|
||||||
def test_create_multiple_objects_with_values(self):
|
def test_create_multiple_objects_with_values(self):
|
||||||
"""
|
"""
|
||||||
@ -446,7 +691,10 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'date_field': '2020-01-02',
|
'date_field': '2020-01-02',
|
||||||
'url_field': 'http://example.com/2',
|
'url_field': 'http://example.com/2',
|
||||||
'json_field': '{"foo": 1, "bar": 2}',
|
'json_field': '{"foo": 1, "bar": 2}',
|
||||||
'choice_field': 'Bar',
|
'select_field': 'Bar',
|
||||||
|
'multiselect_field': ['Bar', 'Baz'],
|
||||||
|
'object_field': VLAN.objects.get(vid=2).pk,
|
||||||
|
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
|
||||||
}
|
}
|
||||||
data = (
|
data = (
|
||||||
{
|
{
|
||||||
@ -483,7 +731,13 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
||||||
self.assertEqual(response_cf['json_field'], custom_field_data['json_field'])
|
self.assertEqual(response_cf['json_field'], custom_field_data['json_field'])
|
||||||
self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field'])
|
self.assertEqual(response_cf['select_field'], custom_field_data['select_field'])
|
||||||
|
self.assertEqual(response_cf['multiselect_field'], custom_field_data['multiselect_field'])
|
||||||
|
self.assertEqual(response_cf['object_field']['id'], custom_field_data['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
|
custom_field_data['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
site = Site.objects.get(pk=response.data[i]['id'])
|
||||||
@ -494,22 +748,25 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
|
||||||
self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field'])
|
self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field'])
|
||||||
self.assertEqual(site.custom_field_data['choice_field'], custom_field_data['choice_field'])
|
self.assertEqual(site.custom_field_data['select_field'], custom_field_data['select_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiselect_field'], custom_field_data['multiselect_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['object_field'], custom_field_data['object_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['multiobject_field'], custom_field_data['multiobject_field'])
|
||||||
|
|
||||||
def test_update_single_object_with_values(self):
|
def test_update_single_object_with_values(self):
|
||||||
"""
|
"""
|
||||||
Update an object with existing custom field values. Ensure that only the updated custom field values are
|
Update an object with existing custom field values. Ensure that only the updated custom field values are
|
||||||
modified.
|
modified.
|
||||||
"""
|
"""
|
||||||
site = self.sites[1]
|
site2 = Site.objects.get(name='Site 2')
|
||||||
original_cfvs = {**site.custom_field_data}
|
original_cfvs = {**site2.custom_field_data}
|
||||||
data = {
|
data = {
|
||||||
'custom_fields': {
|
'custom_fields': {
|
||||||
'text_field': 'ABCD',
|
'text_field': 'ABCD',
|
||||||
'number_field': 1234,
|
'number_field': 1234,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
self.add_permissions('dcim.change_site')
|
self.add_permissions('dcim.change_site')
|
||||||
|
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
@ -524,26 +781,37 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
|
self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
|
self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
|
||||||
self.assertEqual(response_cf['json_field'], original_cfvs['json_field'])
|
self.assertEqual(response_cf['json_field'], original_cfvs['json_field'])
|
||||||
self.assertEqual(response_cf['choice_field'], original_cfvs['choice_field'])
|
self.assertEqual(response_cf['select_field'], original_cfvs['select_field'])
|
||||||
|
self.assertEqual(response_cf['multiselect_field'], original_cfvs['multiselect_field'])
|
||||||
|
self.assertEqual(response_cf['object_field']['id'], original_cfvs['object_field'])
|
||||||
|
self.assertEqual(
|
||||||
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
|
original_cfvs['multiobject_field']
|
||||||
|
)
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site.refresh_from_db()
|
site2.refresh_from_db()
|
||||||
self.assertEqual(site.custom_field_data['text_field'], data['custom_fields']['text_field'])
|
self.assertEqual(site2.custom_field_data['text_field'], data['custom_fields']['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], data['custom_fields']['number_field'])
|
self.assertEqual(site2.custom_field_data['number_field'], data['custom_fields']['number_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
|
self.assertEqual(site2.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
|
self.assertEqual(site2.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
|
||||||
self.assertEqual(site.custom_field_data['date_field'], original_cfvs['date_field'])
|
self.assertEqual(site2.custom_field_data['date_field'], original_cfvs['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], original_cfvs['url_field'])
|
self.assertEqual(site2.custom_field_data['url_field'], original_cfvs['url_field'])
|
||||||
self.assertEqual(site.custom_field_data['json_field'], original_cfvs['json_field'])
|
self.assertEqual(site2.custom_field_data['json_field'], original_cfvs['json_field'])
|
||||||
self.assertEqual(site.custom_field_data['choice_field'], original_cfvs['choice_field'])
|
self.assertEqual(site2.custom_field_data['select_field'], original_cfvs['select_field'])
|
||||||
|
self.assertEqual(site2.custom_field_data['multiselect_field'], original_cfvs['multiselect_field'])
|
||||||
|
self.assertEqual(site2.custom_field_data['object_field'], original_cfvs['object_field'])
|
||||||
|
self.assertEqual(site2.custom_field_data['multiobject_field'], original_cfvs['multiobject_field'])
|
||||||
|
|
||||||
def test_minimum_maximum_values_validation(self):
|
def test_minimum_maximum_values_validation(self):
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
site2 = Site.objects.get(name='Site 2')
|
||||||
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
self.add_permissions('dcim.change_site')
|
self.add_permissions('dcim.change_site')
|
||||||
|
|
||||||
self.cf_integer.validation_minimum = 10
|
cf_integer = CustomField.objects.get(name='number_field')
|
||||||
self.cf_integer.validation_maximum = 20
|
cf_integer.validation_minimum = 10
|
||||||
self.cf_integer.save()
|
cf_integer.validation_maximum = 20
|
||||||
|
cf_integer.save()
|
||||||
|
|
||||||
data = {'custom_fields': {'number_field': 9}}
|
data = {'custom_fields': {'number_field': 9}}
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
@ -558,11 +826,13 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_regex_validation(self):
|
def test_regex_validation(self):
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
site2 = Site.objects.get(name='Site 2')
|
||||||
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
self.add_permissions('dcim.change_site')
|
self.add_permissions('dcim.change_site')
|
||||||
|
|
||||||
self.cf_text.validation_regex = r'^[A-Z]{3}$' # Three uppercase letters
|
cf_text = CustomField.objects.get(name='text_field')
|
||||||
self.cf_text.save()
|
cf_text.validation_regex = r'^[A-Z]{3}$' # Three uppercase letters
|
||||||
|
cf_text.save()
|
||||||
|
|
||||||
data = {'custom_fields': {'text_field': 'ABC123'}}
|
data = {'custom_fields': {'text_field': 'ABC123'}}
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
@ -597,6 +867,9 @@ class CustomFieldImportTest(TestCase):
|
|||||||
CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[
|
CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[
|
||||||
'Choice A', 'Choice B', 'Choice C',
|
'Choice A', 'Choice B', 'Choice C',
|
||||||
]),
|
]),
|
||||||
|
CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[
|
||||||
|
'Choice A', 'Choice B', 'Choice C',
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.save()
|
cf.save()
|
||||||
@ -607,19 +880,20 @@ class CustomFieldImportTest(TestCase):
|
|||||||
Import a Site in CSV format, including a value for each CustomField.
|
Import a Site in CSV format, including a value for each CustomField.
|
||||||
"""
|
"""
|
||||||
data = (
|
data = (
|
||||||
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select'),
|
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
||||||
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A'),
|
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
|
||||||
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B'),
|
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
|
||||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', ''),
|
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''),
|
||||||
)
|
)
|
||||||
csv_data = '\n'.join(','.join(row) for row in data)
|
csv_data = '\n'.join(','.join(row) for row in data)
|
||||||
|
|
||||||
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
|
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Site.objects.count(), 3)
|
||||||
|
|
||||||
# Validate data for site 1
|
# Validate data for site 1
|
||||||
site1 = Site.objects.get(name='Site 1')
|
site1 = Site.objects.get(name='Site 1')
|
||||||
self.assertEqual(len(site1.custom_field_data), 8)
|
self.assertEqual(len(site1.custom_field_data), 9)
|
||||||
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
||||||
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
||||||
self.assertEqual(site1.custom_field_data['integer'], 123)
|
self.assertEqual(site1.custom_field_data['integer'], 123)
|
||||||
@ -628,10 +902,11 @@ class CustomFieldImportTest(TestCase):
|
|||||||
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
||||||
self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
|
self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
|
||||||
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
|
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
|
||||||
|
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B'])
|
||||||
|
|
||||||
# Validate data for site 2
|
# Validate data for site 2
|
||||||
site2 = Site.objects.get(name='Site 2')
|
site2 = Site.objects.get(name='Site 2')
|
||||||
self.assertEqual(len(site2.custom_field_data), 8)
|
self.assertEqual(len(site2.custom_field_data), 9)
|
||||||
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
||||||
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
||||||
self.assertEqual(site2.custom_field_data['integer'], 456)
|
self.assertEqual(site2.custom_field_data['integer'], 456)
|
||||||
@ -640,6 +915,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
||||||
self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
|
self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
|
||||||
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
|
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
|
||||||
|
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C'])
|
||||||
|
|
||||||
# No custom field data should be set for site 3
|
# No custom field data should be set for site 3
|
||||||
site3 = Site.objects.get(name='Site 3')
|
site3 = Site.objects.get(name='Site 3')
|
||||||
|
@ -100,6 +100,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
content_type=content_types[0],
|
content_type=content_types[0],
|
||||||
|
enabled=True,
|
||||||
weight=100,
|
weight=100,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
@ -108,6 +109,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
content_type=content_types[1],
|
content_type=content_types[1],
|
||||||
|
enabled=True,
|
||||||
weight=200,
|
weight=200,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
@ -116,6 +118,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
content_type=content_types[2],
|
content_type=content_types[2],
|
||||||
|
enabled=False,
|
||||||
weight=300,
|
weight=300,
|
||||||
new_window=True,
|
new_window=True,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
@ -136,6 +139,12 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'weight': [100, 200]}
|
params = {'weight': [100, 200]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_enabled(self):
|
||||||
|
params = {'enabled': True}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'enabled': False}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_new_window(self):
|
def test_new_window(self):
|
||||||
params = {'new_window': False}
|
params = {'new_window': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -38,10 +38,27 @@ class CustomFieldModelFormTest(TestCase):
|
|||||||
cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES)
|
cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES)
|
||||||
cf_select.content_types.set([obj_type])
|
cf_select.content_types.set([obj_type])
|
||||||
|
|
||||||
cf_multiselect = CustomField.objects.create(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
cf_multiselect = CustomField.objects.create(
|
||||||
choices=CHOICES)
|
name='multiselect',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||||
|
choices=CHOICES
|
||||||
|
)
|
||||||
cf_multiselect.content_types.set([obj_type])
|
cf_multiselect.content_types.set([obj_type])
|
||||||
|
|
||||||
|
cf_object = CustomField.objects.create(
|
||||||
|
name='object',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
|
object_type=ContentType.objects.get_for_model(Site)
|
||||||
|
)
|
||||||
|
cf_object.content_types.set([obj_type])
|
||||||
|
|
||||||
|
cf_multiobject = CustomField.objects.create(
|
||||||
|
name='multiobject',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
|
object_type=ContentType.objects.get_for_model(Site)
|
||||||
|
)
|
||||||
|
cf_multiobject.content_types.set([obj_type])
|
||||||
|
|
||||||
def test_empty_values(self):
|
def test_empty_values(self):
|
||||||
"""
|
"""
|
||||||
Test that empty custom field values are stored as null
|
Test that empty custom field values are stored as null
|
||||||
|
@ -59,14 +59,15 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
CustomLink.objects.bulk_create((
|
CustomLink.objects.bulk_create((
|
||||||
CustomLink(name='Custom Link 1', content_type=site_ct, link_text='Link 1', link_url='http://example.com/?1'),
|
CustomLink(name='Custom Link 1', content_type=site_ct, enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
||||||
CustomLink(name='Custom Link 2', content_type=site_ct, link_text='Link 2', link_url='http://example.com/?2'),
|
CustomLink(name='Custom Link 2', content_type=site_ct, enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
|
||||||
CustomLink(name='Custom Link 3', content_type=site_ct, link_text='Link 3', link_url='http://example.com/?3'),
|
CustomLink(name='Custom Link 3', content_type=site_ct, enabled=False, link_text='Link 3', link_url='http://example.com/?3'),
|
||||||
))
|
))
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Custom Link X',
|
'name': 'Custom Link X',
|
||||||
'content_type': site_ct.pk,
|
'content_type': site_ct.pk,
|
||||||
|
'enabled': False,
|
||||||
'weight': 100,
|
'weight': 100,
|
||||||
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
||||||
'link_text': 'Link X',
|
'link_text': 'Link X',
|
||||||
@ -74,14 +75,15 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,content_type,weight,button_class,link_text,link_url",
|
"name,content_type,enabled,weight,button_class,link_text,link_url",
|
||||||
"Custom Link 4,dcim.site,100,blue,Link 4,http://exmaple.com/?4",
|
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
|
||||||
"Custom Link 5,dcim.site,100,blue,Link 5,http://exmaple.com/?5",
|
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
|
||||||
"Custom Link 6,dcim.site,100,blue,Link 6,http://exmaple.com/?6",
|
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'button_class': CustomLinkButtonClassChoices.CYAN,
|
'button_class': CustomLinkButtonClassChoices.CYAN,
|
||||||
|
'enabled': False,
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import collections
|
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from taggit.managers import _TaggableManager
|
from taggit.managers import _TaggableManager
|
||||||
@ -57,21 +55,9 @@ class FeatureQuery:
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
def extras_features(*features):
|
def register_features(model, features):
|
||||||
"""
|
|
||||||
Decorator used to register extras provided features to a model
|
|
||||||
"""
|
|
||||||
def wrapper(model_class):
|
|
||||||
# Initialize the model_features store if not already defined
|
|
||||||
if 'model_features' not in registry:
|
|
||||||
registry['model_features'] = {
|
|
||||||
f: collections.defaultdict(list) for f in EXTRAS_FEATURES
|
|
||||||
}
|
|
||||||
for feature in features:
|
for feature in features:
|
||||||
if feature in EXTRAS_FEATURES:
|
if feature not in EXTRAS_FEATURES:
|
||||||
app_label, model_name = model_class._meta.label_lower.split('.')
|
raise ValueError(f"{feature} is not a valid extras feature!")
|
||||||
registry['model_features'][feature][app_label].append(model_name)
|
app_label, model_name = model._meta.label_lower.split('.')
|
||||||
else:
|
registry['model_features'][feature][app_label].add(model_name)
|
||||||
raise ValueError('{} is not a valid extras feature!'.format(feature))
|
|
||||||
return model_class
|
|
||||||
return wrapper
|
|
||||||
|
@ -11,7 +11,7 @@ from rq import Worker
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.tables import paginate_table
|
from netbox.tables import configure_table
|
||||||
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
|
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
|
||||||
from utilities.views import ContentTypePermissionRequiredMixin
|
from utilities.views import ContentTypePermissionRequiredMixin
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
@ -215,7 +215,7 @@ class TagView(generic.ObjectView):
|
|||||||
data=tagged_items,
|
data=tagged_items,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
paginate_table(taggeditem_table, request)
|
configure_table(taggeditem_table, request)
|
||||||
|
|
||||||
object_types = [
|
object_types = [
|
||||||
{
|
{
|
||||||
@ -451,7 +451,7 @@ class ObjectChangeLogView(View):
|
|||||||
data=objectchanges,
|
data=objectchanges,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
paginate_table(objectchanges_table, request)
|
configure_table(objectchanges_table, request)
|
||||||
|
|
||||||
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
|
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
|
||||||
# fall back to using base.html.
|
# fall back to using base.html.
|
||||||
@ -571,7 +571,7 @@ class ObjectJournalView(View):
|
|||||||
assigned_object_id=obj.pk
|
assigned_object_id=obj.pk
|
||||||
)
|
)
|
||||||
journalentry_table = tables.ObjectJournalTable(journalentries)
|
journalentry_table = tables.ObjectJournalTable(journalentries)
|
||||||
paginate_table(journalentry_table, request)
|
configure_table(journalentry_table, request)
|
||||||
|
|
||||||
if request.user.has_perm('extras.add_journalentry'):
|
if request.user.has_perm('extras.add_journalentry'):
|
||||||
form = forms.JournalEntryForm(
|
form = forms.JournalEntryForm(
|
||||||
|
@ -67,7 +67,7 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
|
|||||||
# Prepare the HTTP request
|
# Prepare the HTTP request
|
||||||
params = {
|
params = {
|
||||||
'method': webhook.http_method,
|
'method': webhook.http_method,
|
||||||
'url': webhook.payload_url,
|
'url': webhook.render_payload_url(context),
|
||||||
'headers': headers,
|
'headers': headers,
|
||||||
'data': body.encode('utf8'),
|
'data': body.encode('utf8'),
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ __all__ = [
|
|||||||
'NestedRoleSerializer',
|
'NestedRoleSerializer',
|
||||||
'NestedRouteTargetSerializer',
|
'NestedRouteTargetSerializer',
|
||||||
'NestedServiceSerializer',
|
'NestedServiceSerializer',
|
||||||
|
'NestedServiceTemplateSerializer',
|
||||||
'NestedVLANGroupSerializer',
|
'NestedVLANGroupSerializer',
|
||||||
'NestedVLANSerializer',
|
'NestedVLANSerializer',
|
||||||
'NestedVRFSerializer',
|
'NestedVRFSerializer',
|
||||||
@ -175,6 +176,14 @@ class NestedIPAddressSerializer(WritableNestedSerializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class NestedServiceTemplateSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.ServiceTemplate
|
||||||
|
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
|
||||||
|
|
||||||
|
|
||||||
class NestedServiceSerializer(WritableNestedSerializer):
|
class NestedServiceSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
||||||
|
|
||||||
|
@ -403,6 +403,18 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class ServiceTemplateSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
|
||||||
|
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ServiceTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'ports', 'protocol', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ServiceSerializer(PrimaryModelSerializer):
|
class ServiceSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
||||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
|
@ -42,6 +42,7 @@ router.register('vlan-groups', views.VLANGroupViewSet)
|
|||||||
router.register('vlans', views.VLANViewSet)
|
router.register('vlans', views.VLANViewSet)
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
router.register('service-templates', views.ServiceTemplateViewSet)
|
||||||
router.register('services', views.ServiceViewSet)
|
router.register('services', views.ServiceViewSet)
|
||||||
|
|
||||||
app_name = 'ipam-api'
|
app_name = 'ipam-api'
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user