Merge branch 'feature' into 6651-plugins-rq-queues
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v2.11.7
|
||||
placeholder: v2.11.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v2.11.7
|
||||
placeholder: v2.11.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
5
.github/workflows/ci.yml
vendored
@ -5,7 +5,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: [3.7, 3.8]
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@ -40,6 +40,9 @@ jobs:
|
||||
pip install pycodestyle coverage
|
||||
ln -s configuration.testing.py netbox/netbox/configuration.py
|
||||
|
||||
- name: Collect static files
|
||||
run: python netbox/manage.py collectstatic --no-input
|
||||
|
||||
- name: Check PEP8 compliance
|
||||
run: pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
|
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
node_modules
|
||||
/netbox/project-static/.cache
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/reports/*
|
||||
|
@ -54,11 +54,13 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
### Related projects
|
||||
|
||||
|
@ -2,10 +2,6 @@
|
||||
# https://github.com/django/django
|
||||
Django
|
||||
|
||||
# Django caching using Redis
|
||||
# https://github.com/Suor/django-cacheops
|
||||
django-cacheops
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
django-cors-headers
|
||||
@ -18,6 +14,10 @@ django-debug-toolbar
|
||||
# https://github.com/carltongibson/django-filter
|
||||
django-filter
|
||||
|
||||
# Django debug toolbar extension with support for GraphiQL
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/
|
||||
django-graphiql-debug-toolbar
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt
|
||||
django-mptt
|
||||
@ -30,6 +30,10 @@ django-pglocks
|
||||
# https://github.com/korfuri/django-prometheus
|
||||
django-prometheus
|
||||
|
||||
# Django chaching backend using Redis
|
||||
# https://github.com/jazzband/django-redis
|
||||
django-redis
|
||||
|
||||
# Django integration for RQ (Reqis queuing)
|
||||
# https://github.com/rq/django-rq
|
||||
django-rq
|
||||
@ -54,6 +58,10 @@ djangorestframework
|
||||
# https://github.com/axnsan12/drf-yasg
|
||||
drf-yasg[validation]
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django
|
||||
graphene_django
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://gunicorn.org/
|
||||
gunicorn
|
||||
|
9
contrib/netbox-housekeeping.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# This shell script invokes NetBox's housekeeping management command, which
|
||||
# intended to be run nightly. This script can be copied into your system's
|
||||
# daily cron directory (e.g. /etc/cron.daily), or referenced directly from
|
||||
# within the cron configuration file.
|
||||
#
|
||||
# If NetBox has been installed into a nonstandard location, update the paths
|
||||
# below.
|
||||
/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
|
@ -1,25 +0,0 @@
|
||||
# Caching
|
||||
|
||||
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../configuration/optional-settings.md#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
|
||||
|
||||
If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database.
|
||||
|
||||
## Invalidating Cached Data
|
||||
|
||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py invalidate dcim.Device.34
|
||||
```
|
||||
|
||||
Alternatively, it can also delete all cached results for an object type:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py invalidate dcim.Device
|
||||
```
|
||||
|
||||
Finally, calling it with the `all` argument will force invalidation of the entire cache database:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py invalidate all
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
# 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 configured in the admin UI under Extras > 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.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
10
docs/administration/housekeeping.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Housekeeping
|
||||
|
||||
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be copied into your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside of scheduled execution times will not interfere with its operation.
|
@ -194,7 +194,7 @@ To delete multiple objects at once, call `delete()` on a filtered queryset. It's
|
||||
>>> Device.objects.filter(name__icontains='test').count()
|
||||
27
|
||||
>>> Device.objects.filter(name__icontains='test').delete()
|
||||
(35, {'dcim.DeviceBay': 0, 'secrets.Secret': 0, 'dcim.InterfaceConnection': 4,
|
||||
(35, {'dcim.DeviceBay': 0, 'dcim.InterfaceConnection': 4,
|
||||
'extras.ImageAttachment': 0, 'dcim.Device': 27, 'dcim.Interface': 4,
|
||||
'dcim.ConsolePort': 0, 'dcim.PowerPort': 0})
|
||||
```
|
||||
|
@ -52,14 +52,6 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
---
|
||||
|
||||
## CACHE_TIMEOUT
|
||||
|
||||
Default: 900
|
||||
|
||||
The number of seconds that cache entries will be retained before expiring.
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
@ -96,6 +88,12 @@ CORS_ORIGIN_WHITELIST = [
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic.
|
||||
|
||||
---
|
||||
|
||||
## DEBUG
|
||||
|
||||
Default: False
|
||||
@ -144,7 +142,7 @@ In order to send email, NetBox needs an email server configured. The following i
|
||||
!!! note
|
||||
The `USE_SSL` and `USE_TLS` parameters are mutually exclusive.
|
||||
|
||||
Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration, Django provides a convenient [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail) fuction accessible within the NetBox shell:
|
||||
Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration, Django provides a convenient [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail) function accessible within the NetBox shell:
|
||||
|
||||
```no-highlight
|
||||
# python ./manage.py nbshell
|
||||
@ -195,6 +193,14 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
@ -261,7 +267,7 @@ LOGGING = {
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox but not make any changes.
|
||||
|
||||
---
|
||||
|
||||
@ -472,19 +478,11 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
||||
|
||||
---
|
||||
|
||||
## RELEASE_CHECK_TIMEOUT
|
||||
|
||||
Default: 86,400 (24 hours)
|
||||
|
||||
The number of seconds to retain the latest version that is fetched from the GitHub API before automatically invalidating it and fetching it from the API again. This must be set to at least one hour (3600 seconds).
|
||||
|
||||
---
|
||||
|
||||
## RELEASE_CHECK_URL
|
||||
|
||||
Default: None (disabled)
|
||||
|
||||
This parameter defines the URL of the repository that will be checked periodically for new NetBox releases. When a new release is detected, a message will be displayed to administrative users on the home page. This can be set to the official repository (`'https://api.github.com/repos/netbox-community/netbox/releases'`) or a custom fork. Set this to `None` to disable automatic update checks.
|
||||
This parameter defines the URL of the repository that will be checked for new NetBox releases. When a new release is detected, a message will be displayed to administrative users on the home page. This can be set to the official repository (`'https://api.github.com/repos/netbox-community/netbox/releases'`) or a custom fork. Set this to `None` to disable automatic update checks.
|
||||
|
||||
!!! note
|
||||
The URL provided **must** be compatible with the [GitHub REST API](https://docs.github.com/en/rest).
|
||||
@ -495,7 +493,7 @@ This parameter defines the URL of the repository that will be checked periodical
|
||||
|
||||
Default: `$INSTALL_ROOT/netbox/reports/`
|
||||
|
||||
The file path to the location where custom reports will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
|
||||
The file path to the location where [custom reports](../customization/reports.md) will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
|
||||
|
||||
---
|
||||
|
||||
@ -511,7 +509,7 @@ The maximum execution time of a background task (such as running a custom script
|
||||
|
||||
Default: `$INSTALL_ROOT/netbox/scripts/`
|
||||
|
||||
The file path to the location where custom scripts will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path.
|
||||
The file path to the location where [custom scripts](../customization/custom-scripts.md) will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path.
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Secrets
|
||||
|
||||
{!docs/models/secrets/secret.md!}
|
||||
{!docs/models/secrets/secretrole.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/secrets/userkey.md!}
|
@ -8,7 +8,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
|
||||
|
||||
## Creating Custom Fields
|
||||
|
||||
Custom fields must be created through the admin UI under Extras > Custom Fields. NetBox supports six types of custom field:
|
||||
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||
|
||||
* Text: Free-form text (up to 255 characters)
|
||||
* Integer: A whole number (positive or negative)
|
@ -1,8 +1,8 @@
|
||||
# Custom Links
|
||||
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside of NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
||||
|
||||
Custom links are created under the admin UI. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
||||
|
||||
For example, you might define a link like this:
|
||||
|
||||
@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as:
|
||||
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
|
||||
```
|
||||
|
||||
Custom links appear as buttons at 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.
|
||||
|
||||
## Context Data
|
||||
|
@ -170,14 +170,9 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
|
||||
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
|
||||
|
||||
* `model` - The model class
|
||||
* `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
|
||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||
|
||||
!!! warning
|
||||
The `display_field` parameter is now deprecated, and will be removed in NetBox v3.0. All ObjectVar instances will
|
||||
instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
|
||||
|
||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||
|
||||
```python
|
||||
@ -288,7 +283,6 @@ class NewBranchScript(Script):
|
||||
switch_model = ObjectVar(
|
||||
description="Access switch model",
|
||||
model=DeviceType,
|
||||
display_field='model',
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
86
docs/customization/custom-validation.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Custom Validation
|
||||
|
||||
NetBox validates every object prior to it being written to the database to ensure data integrity. This validation includes things like checking for proper formatting and that references to related objects are valid. However, you may wish to supplement this validation with some rules of your own. For example, perhaps you require that every site's name conforms to a specific pattern. This can be done using NetBox's `CustomValidator` class.
|
||||
|
||||
## CustomValidator
|
||||
|
||||
### Validation Rules
|
||||
|
||||
A custom validator can be instantiated by passing a mapping of attributes to a set of rules to which that attribute must conform. For example:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
CustomValidator({
|
||||
'name': {
|
||||
'min_length': 5,
|
||||
'max_length': 30,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation.
|
||||
|
||||
The `CustomValidator` class supports several validation types:
|
||||
|
||||
* `min`: Minimum value
|
||||
* `max`: Maximum value
|
||||
* `min_length`: Minimum string length
|
||||
* `max_length`: Maximum string length
|
||||
* `regex`: Application of a [regular expression](https://en.wikipedia.org/wiki/Regular_expression)
|
||||
* `required`: A value must be specified
|
||||
* `prohibited`: A value must _not_ be specified
|
||||
|
||||
The `min` and `max` types should be defined for numeric values, whereas `min_length`, `max_length`, and `regex` are suitable for character strings (text values). The `required` and `prohibited` validators may be used for any field, and should be passed a value of `True`.
|
||||
|
||||
!!! warning
|
||||
Bear in mind that these validators merely supplement NetBox's own validation: They will not override it. For example, if a certain model field is required by NetBox, setting a validator for it with `{'prohibited': True}` will not work.
|
||||
|
||||
### Custom Validation Logic
|
||||
|
||||
There may be instances where the provided validation types are insufficient. The `CustomValidator` class can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected.
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
class MyValidator(CustomValidator):
|
||||
def validate(self, instance):
|
||||
if instance.status == 'active' and not instance.description:
|
||||
self.fail("Active sites must have a description set!", field='status')
|
||||
```
|
||||
|
||||
The `fail()` method may optionally specify a field with which to associate the supplied error message. If specified, the error message will appear to the user as associated with this field. If omitted, the error message will not be associated with any field.
|
||||
|
||||
## Assigning Custom Validators
|
||||
|
||||
Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/optional-settings.md#custom_validators) configuration parameter, as such:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
Validator1,
|
||||
Validator2,
|
||||
Validator3
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Even if defining only a single validator, it must be passed as an iterable.
|
||||
|
||||
When it is not necessary to define a custom `validate()` method, you may opt to pass a `CustomValidator` instance directly:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
CustomValidator({
|
||||
'name': {
|
||||
'min_length': 5,
|
||||
'max_length': 30,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
# Export Templates
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates.
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
||||
|
||||
@ -33,6 +33,16 @@ The `as_attachment` attribute of an export template controls its behavior when r
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
||||
## REST API Integration
|
||||
|
||||
When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example:
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?export=MyTemplateName
|
||||
```
|
||||
|
||||
Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list.
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
@ -32,19 +32,15 @@ class Foo(models.Model):
|
||||
raise ValidationError()
|
||||
```
|
||||
|
||||
## 3. Add CSV helpers
|
||||
|
||||
Add the name of the new field to `csv_headers` and included a CSV-friendly representation of its data in the model's `to_csv()` method. These will be used when exporting objects in CSV format.
|
||||
|
||||
## 4. Update relevant querysets
|
||||
## 3. Update relevant querysets
|
||||
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
|
||||
|
||||
## 5. Update API serializer
|
||||
## 4. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
|
||||
|
||||
## 6. Add field to forms
|
||||
## 5. Add field to forms
|
||||
|
||||
Extend any forms to include the new field as appropriate. Common forms include:
|
||||
|
||||
@ -53,19 +49,19 @@ Extend any forms to include the new field as appropriate. Common forms include:
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
## 7. Extend object filter set
|
||||
## 6. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
|
||||
|
||||
## 8. Add column to object table
|
||||
## 7. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column.
|
||||
|
||||
## 9. Update the UI templates
|
||||
## 8. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
## 10. Create/extend test cases
|
||||
## 9. Create/extend test cases
|
||||
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
@ -77,6 +73,6 @@ Create or extend the relevant test cases to verify that the new field and any ac
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
||||
## 11. Update the model's documentation
|
||||
## 10. Update the model's documentation
|
||||
|
||||
Each model has a dedicated page in the documentation, at `models/<app>/<model>.md`. Update this file to include any relevant information about the new field.
|
||||
|
@ -25,7 +25,6 @@ NetBox components are arranged into functional subsections called _apps_ (a carr
|
||||
* `dcim`: Datacenter infrastructure management (sites, racks, and devices)
|
||||
* `extras`: Additional features not considered part of the core data model
|
||||
* `ipam`: IP address management (VRFs, prefixes, IP addresses, and VLANs)
|
||||
* `secrets`: Encrypted storage of sensitive data (e.g. login credentials)
|
||||
* `tenancy`: Tenants (such as customers) to which NetBox objects may be assigned
|
||||
* `users`: Authentication and user preferences
|
||||
* `utilities`: Resources which are not user-facing (extendable classes, etc.)
|
||||
|
@ -10,8 +10,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
|
||||
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
|
||||
* [Custom fields](../additional-features/custom-fields.md) - These models support the addition of user-defined fields
|
||||
* [Export templates](../additional-features/export-templates.md) - Users can create custom export templates for these models
|
||||
* [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields
|
||||
* [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models
|
||||
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
|
||||
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
|
||||
* Nesting - These models can be nested recursively to create a hierarchy
|
||||
@ -47,7 +47,6 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [ipam.Service](../models/ipam/service.md)
|
||||
* [ipam.VLAN](../models/ipam/vlan.md)
|
||||
* [ipam.VRF](../models/ipam/vrf.md)
|
||||
* [secrets.Secret](../models/secrets/secret.md)
|
||||
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
||||
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
||||
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
||||
@ -62,7 +61,6 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [ipam.RIR](../models/ipam/rir.md)
|
||||
* [ipam.Role](../models/ipam/role.md)
|
||||
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
|
||||
* [secrets.SecretRole](../models/secrets/secretrole.md)
|
||||
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
|
||||
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
|
||||
|
||||
|
11
docs/development/signals.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Signals
|
||||
|
||||
In addition to [Django's built-in signals](https://docs.djangoproject.com/en/stable/topics/signals/), NetBox defines some of its own, listed below.
|
||||
|
||||
## post_clean
|
||||
|
||||
This signal is sent by models which inherit from `CustomValidationMixin` at the end of their `clean()` method.
|
||||
|
||||
### Receivers
|
||||
|
||||
* `extras.signals.run_custom_validators()`
|
70
docs/graphql-api/overview.md
Normal file
@ -0,0 +1,70 @@
|
||||
# GraphQL API Overview
|
||||
|
||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by the [Graphene](https://graphene-python.org/) library and [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/).
|
||||
|
||||
## Queries
|
||||
|
||||
GraphQL enables the client to specify an arbitrary nested list of fields to include in the response. All queries are made to the root `/graphql` API endpoint. For example, to return the circuit ID and provider name of each circuit with an active status, you can issue a request such as the following:
|
||||
|
||||
```
|
||||
curl -H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
http://netbox/graphql/ \
|
||||
--data '{"query": "query {circuits(status:\"active\" {cid provider {name}}}"}'
|
||||
```
|
||||
|
||||
The response will include the requested data formatted as JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"circuits": [
|
||||
{
|
||||
"cid": "1002840283",
|
||||
"provider": {
|
||||
"name": "CenturyLink"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cid": "1002840457",
|
||||
"provider": {
|
||||
"name": "CenturyLink"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
It's recommended to pass the return data through a JSON parser such as `jq` for better readability.
|
||||
|
||||
NetBox provides both a singular and plural query field for each object type:
|
||||
|
||||
* `$OBJECT`: Returns a single object. Must specify the object's unique ID as `(id: 123)`.
|
||||
* `$OBJECT_list`: Returns a list of objects, optionally filtered by given parameters.
|
||||
|
||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of fitlers) to fetch all devices.
|
||||
|
||||
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/).
|
||||
|
||||
## Filtering
|
||||
|
||||
The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active:
|
||||
|
||||
```
|
||||
{"query": "query {sites(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form:
|
||||
|
||||
```
|
||||
Authorization: Token $TOKEN
|
||||
```
|
||||
|
||||
## Disabling the GraphQL API
|
||||
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/optional-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
@ -247,6 +247,18 @@ Password (again):
|
||||
Superuser created successfully.
|
||||
```
|
||||
|
||||
## Schedule the Housekeeping Task
|
||||
|
||||
NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
|
||||
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
## Test the Application
|
||||
|
||||
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance:
|
||||
|
@ -102,5 +102,12 @@ Finally, restart the gunicorn and RQ services:
|
||||
sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
!!! note
|
||||
If upgrading from an installation that uses supervisord, please see the instructions for [migrating to systemd](migrating-to-systemd.md). The use of supervisord is no longer supported.
|
||||
## Verify Housekeeping Scheduling
|
||||
|
||||
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
BIN
docs/media/cable-dark.png
Normal file
After Width: | Height: | Size: 459 KiB |
BIN
docs/media/cable-light.png
Normal file
After Width: | Height: | Size: 467 KiB |
BIN
docs/media/home-dark.png
Normal file
After Width: | Height: | Size: 769 KiB |
BIN
docs/media/home-light.png
Normal file
After Width: | Height: | Size: 819 KiB |
BIN
docs/media/prefixes-dark.png
Normal file
After Width: | Height: | Size: 916 KiB |
BIN
docs/media/prefixes-light.png
Normal file
After Width: | Height: | Size: 911 KiB |
BIN
docs/media/rack-dark.png
Normal file
After Width: | Height: | Size: 559 KiB |
BIN
docs/media/rack-light.png
Normal file
After Width: | Height: | Size: 569 KiB |
Before Width: | Height: | Size: 336 KiB |
Before Width: | Height: | Size: 336 KiB |
Before Width: | Height: | Size: 339 KiB |
@ -2,7 +2,7 @@
|
||||
|
||||
A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.
|
||||
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, secrets, services, and other attributes related to managing the VC.
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, and other attributes related to managing the VC.
|
||||
|
||||
!!! note
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices.
|
||||
|
@ -1,5 +0,0 @@
|
||||
# Secrets
|
||||
|
||||
A secret represents a single credential or other sensitive string of characters which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
|
||||
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
@ -1,9 +0,0 @@
|
||||
# Secret Roles
|
||||
|
||||
Each secret is assigned a functional role which indicates what it is used for. Secret roles are customizable. Typical roles might include:
|
||||
|
||||
* Login credentials
|
||||
* SNMP community strings
|
||||
* RADIUS/TACACS+ keys
|
||||
* IKE key strings
|
||||
* Routing protocol shared secrets
|
@ -1,35 +0,0 @@
|
||||
# User Keys
|
||||
|
||||
Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
|
||||
|
||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
|
||||
|
||||
## Supported Key Format
|
||||
|
||||
Public key formats supported
|
||||
|
||||
- PKCS#1 RSAPublicKey* (PEM header: BEGIN RSA PUBLIC KEY)
|
||||
- X.509 SubjectPublicKeyInfo** (PEM header: BEGIN PUBLIC KEY)
|
||||
- **OpenSSH line format is not supported.**
|
||||
|
||||
Private key formats supported (unencrypted)
|
||||
|
||||
- PKCS#1 RSAPrivateKey** (PEM header: BEGIN RSA PRIVATE KEY)
|
||||
- PKCS#8 PrivateKeyInfo* (PEM header: BEGIN PRIVATE KEY)
|
||||
|
||||
|
||||
## Creating the First User Key
|
||||
|
||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.
|
||||
|
||||
To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
|
||||
|
||||
When the first user key is created in NetBox, a random master encryption key is generated automatically. This key is then encrypted using the public key provided and stored as part of your user key. **The master key cannot be recovered** without your private key.
|
||||
|
||||
Once a user key has been assigned an encrypted copy of the master key, it is considered activated and can now be used to encrypt and decrypt secrets.
|
||||
|
||||
## Creating Additional User Keys
|
||||
|
||||
Any user can create his or her user key by generating or uploading a public RSA key. However, a user key cannot be used to encrypt or decrypt secrets until it has been activated with an encrypted copy of the master key.
|
||||
|
||||
Only an administrator with an active user key can activate other user keys. To do so, access the NetBox admin UI and navigate to Secrets > User Keys. Select the user key(s) to be activated, and select "activate selected user keys" from the actions dropdown. You will need to provide your private key in order to decrypt the master key. A copy of the master key is then encrypted using the public key associated with the user key being activated.
|
@ -113,7 +113,6 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
| `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 |
|
||||
| `caching_config` | Plugin-specific cache configuration
|
||||
| `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`) |
|
||||
|
||||
@ -386,34 +385,6 @@ class SiteAnimalCount(PluginTemplateExtension):
|
||||
template_extensions = [SiteAnimalCount]
|
||||
```
|
||||
|
||||
## Caching Configuration
|
||||
|
||||
By default, all query operations within a plugin are cached. To change this, define a caching configuration under the PluginConfig class' `caching_config` attribute. All configuration keys will be applied within the context of the plugin; there is no need to include the plugin name. An example configuration is below:
|
||||
|
||||
```python
|
||||
class MyPluginConfig(PluginConfig):
|
||||
...
|
||||
caching_config = {
|
||||
'foo': {
|
||||
'ops': 'get',
|
||||
'timeout': 60 * 15,
|
||||
},
|
||||
'*': {
|
||||
'ops': 'all',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To disable caching for your plugin entirely, set:
|
||||
|
||||
```python
|
||||
caching_config = {
|
||||
'*': None
|
||||
}
|
||||
```
|
||||
|
||||
See the [django-cacheops](https://github.com/Suor/django-cacheops) documentation for more detail on configuring caching.
|
||||
|
||||
## Background Tasks
|
||||
|
||||
By default, Netbox provides 3 differents [RQ](https://python-rq.org/) queues to run background jobs : *high*, *default* and *low*.
|
||||
@ -440,4 +411,4 @@ In case you create dedicated queues for your plugin, it is strongly advised to a
|
||||
|
||||
```
|
||||
python manage.py rqworker myplugin.queue1 myplugin.queue2 myplugin.queue-whatever-the-name
|
||||
```
|
||||
```
|
||||
|
@ -1 +1 @@
|
||||
version-2.11.md
|
||||
version-3.0.md
|
@ -1,10 +1,26 @@
|
||||
# NetBox v2.11
|
||||
|
||||
## v2.11.8 (FUTURE)
|
||||
## v2.11.9 (2021-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6456](https://github.com/netbox-community/netbox/issues/6456) - API schema type should be boolean for `_occupied` on cable termination models
|
||||
* [#6710](https://github.com/netbox-community/netbox/issues/6710) - Fix assignment of VM interface parent via REST API
|
||||
* [#6714](https://github.com/netbox-community/netbox/issues/6714) - Fix rendering of device type component creation forms
|
||||
|
||||
---
|
||||
|
||||
## v2.11.8 (2021-07-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5503](https://github.com/netbox-community/netbox/issues/5503) - Annotate short date & time fields with their longer form
|
||||
* [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields
|
||||
* [#6200](https://github.com/netbox-community/netbox/issues/6200) - Add rack reservations to global search
|
||||
* [#6368](https://github.com/netbox-community/netbox/issues/6368) - Enable virtual chassis assignment during bulk import of devices
|
||||
* [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view
|
||||
* [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view
|
||||
* [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@ -12,6 +28,9 @@
|
||||
* [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view
|
||||
* [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields
|
||||
* [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices
|
||||
* [#6676](https://github.com/netbox-community/netbox/issues/6676) - Fix device/VM counts per cluster under cluster type/group views
|
||||
* [#6680](https://github.com/netbox-community/netbox/issues/6680) - Allow setting custom field values for VM interfaces on initial creation
|
||||
* [#6695](https://github.com/netbox-community/netbox/issues/6695) - Fix exception when importing device type with invalid front port definition
|
||||
|
||||
---
|
||||
|
||||
|
@ -218,7 +218,7 @@
|
||||
|
||||
#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415))
|
||||
|
||||
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/additional-features/custom-scripts/) for more detail.
|
||||
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/customization/custom-scripts/) for more detail.
|
||||
|
||||
Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release.
|
||||
|
||||
|
115
docs/release-notes/version-3.0.md
Normal file
@ -0,0 +1,115 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## v3.0-beta1 (FUTURE)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The default CSV export format for all objects now includes all available data. Additionally, the CSV headers now use human-friendly titles rather than the raw field names.
|
||||
* Support for queryset caching configuration (`caching_config`) has been removed from the plugins API (see [#6639](https://github.com/netbox-community/netbox/issues/6639)).
|
||||
* The `cacheops_*` metrics have been removed from the Prometheus exporter (see [#6639](https://github.com/netbox-community/netbox/issues/6639)).
|
||||
* The `invalidate` management command has been removed.
|
||||
* The redundant REST API endpoints for console, power, and interface connections have been removed. The same data can be retrieved using the respective model endpoints with the `?connected=True` filter applied.
|
||||
|
||||
### New Features
|
||||
|
||||
### REST API Token Provisioning ([#5264](https://github.com/netbox-community/netbox/issues/5264))
|
||||
|
||||
This release introduces the `/api/users/tokens/` REST API endpoint, which includes a child endpoint that can be employed by a user to provision a new REST API token. This allows a user to gain REST API access without needing to first create a token via the web UI.
|
||||
|
||||
```
|
||||
$ curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
https://netbox/api/users/tokens/provision/
|
||||
{
|
||||
"username": "hankhill",
|
||||
"password: "I<3C3H8",
|
||||
}
|
||||
```
|
||||
|
||||
If the supplied credentials are valid, NetBox will create and return a new token for the user.
|
||||
|
||||
#### Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963))
|
||||
|
||||
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/optional-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
CustomValidator({
|
||||
'name': {
|
||||
'min_length': 10,
|
||||
},
|
||||
'description': {
|
||||
'required': True,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
CustomValidator can also be subclassed to enforce more complex logic by overriding its `validate()` method. See the [custom validation](../customization/custom-validation.md) documentation for more details.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#2434](https://github.com/netbox-community/netbox/issues/2434) - Add option to assign IP address upon creating a new interface
|
||||
* [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API
|
||||
* [#3682](https://github.com/netbox-community/netbox/issues/3682) - Add `color` field to front and rear ports
|
||||
* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized
|
||||
* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit
|
||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||
* [#6590](https://github.com/netbox-community/netbox/issues/6590) - Introduce a nightly housekeeping command to clear expired sessions and change records
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#5223](https://github.com/netbox-community/netbox/issues/5223) - Remove the console/power/interface connections REST API endpoints
|
||||
* [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6
|
||||
* [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar
|
||||
* [#6068](https://github.com/netbox-community/netbox/issues/6068) - Drop support for legacy static CSV export
|
||||
* [#6338](https://github.com/netbox-community/netbox/issues/6338) - Decimal fields are no longer coerced to strings in REST API
|
||||
* [#6639](https://github.com/netbox-community/netbox/issues/6639) - Drop support for queryset caching (django-cacheops)
|
||||
* [#6713](https://github.com/netbox-community/netbox/issues/6713) - Checking for new releases is now done as part of the housekeeping routine
|
||||
|
||||
### Configuration Changes
|
||||
|
||||
* The `CACHE_TIMEOUT` configuration parameter has been removed.
|
||||
* The `RELEASE_CHECK_TIMEOUT` configuration parameter has been removed.
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Added the `/api/users/tokens/` endpoint
|
||||
* The `provision/` child endpoint can be used to provision new REST API tokens by supplying a valid username and password
|
||||
* Removed the following "connections" endpoints:
|
||||
* `/api/dcim/console-connections`
|
||||
* `/api/dcim/power-connections`
|
||||
* `/api/dcim/interface-connections`
|
||||
* dcim.Cable
|
||||
* `length` is now a decimal value
|
||||
* dcim.Device
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.DeviceType
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.FrontPort
|
||||
* Added `color` field
|
||||
* dcim.FrontPortTemplate
|
||||
* Added `color` field
|
||||
* dcim.Rack
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.RearPort
|
||||
* Added `color` field
|
||||
* dcim.RearPortTemplate
|
||||
* Added `color` field
|
||||
* dcim.Site
|
||||
* `latitude` and `longitude` are now decimal fields rather than strings
|
||||
* extras.ContentType
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* ipam.Prefix
|
||||
* Added the `mark_utilized` boolean field
|
||||
* ipam.VLAN
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* ipam.VRF
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* virtualization.VirtualMachine
|
||||
* `vcpus` is now a decimal field rather than a string
|
@ -11,7 +11,7 @@ An authentication token is attached to a request by setting the `Authorization`
|
||||
```
|
||||
$ curl -H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
http://netbox/api/dcim/sites/
|
||||
https://netbox/api/dcim/sites/
|
||||
{
|
||||
"count": 10,
|
||||
"next": null,
|
||||
@ -23,8 +23,46 @@ http://netbox/api/dcim/sites/
|
||||
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
|
||||
|
||||
```
|
||||
$ curl http://netbox/api/dcim/sites/
|
||||
$ curl https://netbox/api/dcim/sites/
|
||||
{
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
```
|
||||
|
||||
## Initial Token Provisioning
|
||||
|
||||
Ideally, each user should provision his or her own REST API token(s) via the web UI. However, you may encounter where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination.
|
||||
|
||||
To provision a token via the REST API, make a `POST` request to the `/api/users/tokens/provision/` endpoint:
|
||||
|
||||
```
|
||||
$ curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
https://netbox/api/users/tokens/provision/
|
||||
{
|
||||
"username": "hankhill",
|
||||
"password: "I<3C3H8",
|
||||
}
|
||||
```
|
||||
|
||||
Note that we are _not_ passing an existing REST API token with this request. If the supplied credentials are valid, a new REST API token will be automatically created for the user. Note that the key will be automatically generated, and write ability will be enabled.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 6,
|
||||
"url": "https://netbox/api/users/tokens/6/",
|
||||
"display": "3c9cb9 (hankhill)",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"url": "https://netbox/api/users/users/2/",
|
||||
"display": "hankhill",
|
||||
"username": "hankhill"
|
||||
},
|
||||
"created": "2021-06-11T20:09:13.339367Z",
|
||||
"expires": null,
|
||||
"key": "9fc9b897abec9ada2da6aec9dbc34596293c9cb9",
|
||||
"write_enabled": true,
|
||||
"description": ""
|
||||
}
|
||||
```
|
||||
|
@ -61,25 +61,30 @@ These lookup expressions can be applied by adding a suffix to the desired field'
|
||||
|
||||
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
|
||||
|
||||
- `n` - not equal to (negation)
|
||||
- `lt` - less than
|
||||
- `lte` - less than or equal
|
||||
- `gt` - greater than
|
||||
- `gte` - greater than or equal
|
||||
| Filter | Description |
|
||||
|--------|-------------|
|
||||
| `n` | Not equal to |
|
||||
| `lt` | Less than |
|
||||
| `lte` | Less than or equal to |
|
||||
| `gt` | Greater than |
|
||||
| `gte` | Greater than or equal to |
|
||||
|
||||
### String Fields
|
||||
|
||||
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
||||
|
||||
- `n` - not equal to (negation)
|
||||
- `ic` - case insensitive contains
|
||||
- `nic` - negated case insensitive contains
|
||||
- `isw` - case insensitive starts with
|
||||
- `nisw` - negated case insensitive starts with
|
||||
- `iew` - case insensitive ends with
|
||||
- `niew` - negated case insensitive ends with
|
||||
- `ie` - case insensitive exact match
|
||||
- `nie` - negated case insensitive exact match
|
||||
| Filter | Description |
|
||||
|--------|-------------|
|
||||
| `n` | Not equal to |
|
||||
| `ic` | Contains (case-insensitive) |
|
||||
| `nic` | Does not contain (case-insensitive) |
|
||||
| `isw` | Starts with (case-insensitive) |
|
||||
| `nisw` | Does not start with (case-insensitive) |
|
||||
| `iew` | Ends with (case-insensitive) |
|
||||
| `niew` | Does not end with (case-insensitive) |
|
||||
| `ie` | Exact match (case-insensitive) |
|
||||
| `nie` | Inverse exact match (case-insensitive) |
|
||||
| `empty` | Is empty (boolean) |
|
||||
|
||||
### Foreign Keys & Other Fields
|
||||
|
||||
|
@ -67,7 +67,7 @@ Comprehensive, interactive documentation of all REST API endpoints is available
|
||||
|
||||
## Endpoint Hierarchy
|
||||
|
||||
NetBox's entire REST API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, plugins, secrets, tenancy, users, and virtualization. Within each application exists a separate path for each model. For example, the provider and circuit objects are located under the "circuits" application:
|
||||
NetBox's entire REST API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, plugins, tenancy, users, and virtualization. Within each application exists a separate path for each model. For example, the provider and circuit objects are located under the "circuits" application:
|
||||
|
||||
* `/api/circuits/providers/`
|
||||
* `/api/circuits/circuits/`
|
||||
|
@ -1,172 +0,0 @@
|
||||
# Working with Secrets
|
||||
|
||||
As with most other objects, the REST API can be used to view, create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data.
|
||||
|
||||
## Generating a Session Key
|
||||
|
||||
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../core-functionality/secrets.md#user-keys). The private key must be POSTed with the name `private_key`.
|
||||
|
||||
```no-highlight
|
||||
$ curl -X POST http://netbox/api/secrets/get-session-key/ \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
--data-urlencode "private_key@<filename>"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"session_key": "dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
To read the private key from a file, use the convention above. Alternatively, the private key can be read from an environment variable using `--data-urlencode "private_key=$PRIVATE_KEY"`.
|
||||
|
||||
The request uses the provided private key to unlock your stored copy of the master key and generate a temporary session key, which can be attached in the `X-Session-Key` header of future API requests.
|
||||
|
||||
## Retrieving Secrets
|
||||
|
||||
A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its `plaintext` field set to null.
|
||||
|
||||
```no-highlight
|
||||
$ curl http://netbox/api/secrets/secrets/2587/ \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2587,
|
||||
"url": "http://netbox/api/secrets/secrets/2587/",
|
||||
"device": {
|
||||
"id": 1827,
|
||||
"url": "http://netbox/api/dcim/devices/1827/",
|
||||
"name": "MyTestDevice",
|
||||
"display_name": "MyTestDevice"
|
||||
},
|
||||
"role": {
|
||||
"id": 1,
|
||||
"url": "http://netbox/api/secrets/secret-roles/1/",
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-creds"
|
||||
},
|
||||
"name": "admin",
|
||||
"plaintext": null,
|
||||
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2017-03-21",
|
||||
"last_updated": "2017-03-21T19:28:44.265582Z"
|
||||
}
|
||||
```
|
||||
|
||||
To decrypt a secret, we must include our session key in the `X-Session-Key` header when sending the `GET` request:
|
||||
|
||||
```no-highlight
|
||||
$ curl http://netbox/api/secrets/secrets/2587/ \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2587,
|
||||
"url": "http://netbox/api/secrets/secrets/2587/",
|
||||
"device": {
|
||||
"id": 1827,
|
||||
"url": "http://netbox/api/dcim/devices/1827/",
|
||||
"name": "MyTestDevice",
|
||||
"display_name": "MyTestDevice"
|
||||
},
|
||||
"role": {
|
||||
"id": 1,
|
||||
"url": "http://netbox/api/secrets/secret-roles/1/",
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-creds"
|
||||
},
|
||||
"name": "admin",
|
||||
"plaintext": "foobar",
|
||||
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2017-03-21",
|
||||
"last_updated": "2017-03-21T19:28:44.265582Z"
|
||||
}
|
||||
```
|
||||
|
||||
Multiple secrets within a list can be decrypted in this manner as well:
|
||||
|
||||
```no-highlight
|
||||
$ curl http://netbox/api/secrets/secrets/?limit=3 \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 3482,
|
||||
"next": "http://netbox/api/secrets/secrets/?limit=3&offset=3",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 2587,
|
||||
"plaintext": "foobar",
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 2588,
|
||||
"plaintext": "MyP@ssw0rd!",
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 2589,
|
||||
"plaintext": "AnotherSecret!",
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Creating and Updating Secrets
|
||||
|
||||
Session keys are required when creating or modifying secrets. The secret's `plaintext` attribute is set to its non-encrypted value, and NetBox uses the session key to compute and store the encrypted value.
|
||||
|
||||
```no-highlight
|
||||
$ curl -X POST http://netbox/api/secrets/secrets/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk=" \
|
||||
--data '{"device": 1827, "role": 1, "name": "backup", "plaintext": "Drowssap1"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 6194,
|
||||
"url": "http://netbox/api/secrets/secrets/9194/",
|
||||
"device": {
|
||||
"id": 1827,
|
||||
"url": "http://netbox/api/dcim/devices/1827/",
|
||||
"name": "device43",
|
||||
"display_name": "device43"
|
||||
},
|
||||
"role": {
|
||||
"id": 1,
|
||||
"url": "http://netbox/api/secrets/secret-roles/1/",
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-creds"
|
||||
},
|
||||
"name": "backup",
|
||||
"plaintext": "Drowssap1",
|
||||
"hash": "pbkdf2_sha256$1000$J9db8sI5vBrd$IK6nFXnFl+K+nR5/KY8RSDxU1skYL8G69T5N3jZxM7c=",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2020-08-05",
|
||||
"last_updated": "2020-08-05T16:51:14.990506Z"
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Don't forget to include the `Content-Type: application/json` header when making a POST or PATCH request.
|
36
docs/screenshots/index.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Screenshots
|
||||
|
||||
## Light Mode
|
||||
|
||||
### Home Page
|
||||
|
||||

|
||||
|
||||
### Rack Elevation
|
||||
|
||||

|
||||
|
||||
### Prefixes
|
||||
|
||||

|
||||
|
||||
### Cable Trace
|
||||

|
||||
|
||||
## Dark Mode
|
||||
|
||||
### Home Page
|
||||
|
||||

|
||||
|
||||
### Rack Elevation
|
||||
|
||||

|
||||
|
||||
### Prefixes
|
||||
|
||||

|
||||
|
||||
### Cable Trace
|
||||

|
||||
|
35
mkdocs.yml
@ -6,6 +6,21 @@ python:
|
||||
- requirements: docs/requirements.txt
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/lightbulb-outline
|
||||
name: Switch to Dark Mode
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/lightbulb
|
||||
name: Switch to Light Mode
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/netbox-community/netbox
|
||||
- icon: fontawesome/brands/slack
|
||||
link: https://slack.netbox.dev
|
||||
extra_css:
|
||||
- extra.css
|
||||
markdown_extensions:
|
||||
@ -43,20 +58,21 @@ nav:
|
||||
- Service Mapping: 'core-functionality/services.md'
|
||||
- Circuits: 'core-functionality/circuits.md'
|
||||
- Power Tracking: 'core-functionality/power.md'
|
||||
- Secrets: 'core-functionality/secrets.md'
|
||||
- Tenancy: 'core-functionality/tenancy.md'
|
||||
- Customization:
|
||||
- Custom Fields: 'customization/custom-fields.md'
|
||||
- Custom Validation: 'customization/custom-validation.md'
|
||||
- Custom Links: 'customization/custom-links.md'
|
||||
- Export Templates: 'customization/export-templates.md'
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Reports: 'customization/reports.md'
|
||||
- Additional Features:
|
||||
- Caching: 'additional-features/caching.md'
|
||||
- Change Logging: 'additional-features/change-logging.md'
|
||||
- Context Data: 'models/extras/configcontext.md'
|
||||
- Custom Fields: 'additional-features/custom-fields.md'
|
||||
- Custom Links: 'additional-features/custom-links.md'
|
||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
||||
- Export Templates: 'additional-features/export-templates.md'
|
||||
- Journaling: 'additional-features/journaling.md'
|
||||
- NAPALM: 'additional-features/napalm.md'
|
||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||
- Reports: 'additional-features/reports.md'
|
||||
- Tags: 'models/extras/tag.md'
|
||||
- Webhooks: 'additional-features/webhooks.md'
|
||||
- Plugins:
|
||||
@ -64,23 +80,27 @@ nav:
|
||||
- Developing Plugins: 'plugins/development.md'
|
||||
- Administration:
|
||||
- Permissions: 'administration/permissions.md'
|
||||
- Housekeeping: 'administration/housekeeping.md'
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
- NetBox Shell: 'administration/netbox-shell.md'
|
||||
- REST API:
|
||||
- Overview: 'rest-api/overview.md'
|
||||
- Filtering: 'rest-api/filtering.md'
|
||||
- Authentication: 'rest-api/authentication.md'
|
||||
- Working with Secrets: 'rest-api/working-with-secrets.md'
|
||||
- GraphQL API:
|
||||
- Overview: 'graphql-api/overview.md'
|
||||
- Development:
|
||||
- Introduction: 'development/index.md'
|
||||
- Getting Started: 'development/getting-started.md'
|
||||
- Style Guide: 'development/style-guide.md'
|
||||
- Models: 'development/models.md'
|
||||
- Extending Models: 'development/extending-models.md'
|
||||
- Signals: 'development/signals.md'
|
||||
- Application Registry: 'development/application-registry.md'
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- Release Notes:
|
||||
- Version 3.0: 'release-notes/version-3.0.md'
|
||||
- Version 2.11: 'release-notes/version-2.11.md'
|
||||
- Version 2.10: 'release-notes/version-2.10.md'
|
||||
- Version 2.9: 'release-notes/version-2.9.md'
|
||||
@ -93,3 +113,4 @@ nav:
|
||||
- Version 2.2: 'release-notes/version-2.2.md'
|
||||
- Version 2.1: 'release-notes/version-2.1.md'
|
||||
- Version 2.0: 'release-notes/version-2.0.md'
|
||||
- Screenshots: 'screenshots/index.md'
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
@ -60,10 +60,12 @@ class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = Provider.csv_headers
|
||||
fields = (
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -102,12 +104,12 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi
|
||||
]
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = Provider
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['region_id', 'site_id'],
|
||||
['asn', 'tag'],
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -166,7 +168,7 @@ class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -190,13 +192,9 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = ProviderNetwork
|
||||
field_order = ['q', 'provider_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_order = ['provider_id']
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
@ -219,7 +217,7 @@ class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -238,7 +236,7 @@ class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = CircuitType.csv_headers
|
||||
fields = ('name', 'slug', 'description')
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
@ -312,7 +310,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -354,16 +352,19 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
]
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Circuit
|
||||
field_order = [
|
||||
'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
|
||||
'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
|
||||
'commit_rate',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['type_id', 'status', 'commit_rate'],
|
||||
['provider_id', 'provider_network_id'],
|
||||
['region_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['tag']
|
||||
]
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
|
21
netbox/circuits/graphql/schema.py
Normal file
@ -0,0 +1,21 @@
|
||||
import graphene
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
|
||||
circuit_termination = ObjectField(CircuitTerminationType)
|
||||
circuit_termination_list = ObjectListField(CircuitTerminationType)
|
||||
|
||||
circuit_type = ObjectField(CircuitTypeType)
|
||||
circuit_type_list = ObjectListField(CircuitTypeType)
|
||||
|
||||
provider = ObjectField(ProviderType)
|
||||
provider_list = ObjectListField(ProviderType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_network_list = ObjectListField(ProviderNetworkType)
|
50
netbox/circuits/graphql/types.py
Normal file
@ -0,0 +1,50 @@
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
'CircuitType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderNetworkType',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitTermination
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
class CircuitType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
class CircuitTypeType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CircuitType
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
class ProviderType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Provider
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
class ProviderNetworkType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderNetwork
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
@ -63,9 +63,6 @@ class Provider(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
]
|
||||
@ -79,18 +76,6 @@ class Provider(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.asn,
|
||||
self.account,
|
||||
self.portal_url,
|
||||
self.noc_contact,
|
||||
self.admin_contact,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
@ -118,10 +103,6 @@ class ProviderNetwork(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
]
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
@ -140,14 +121,6 @@ class ProviderNetwork(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.provider.name,
|
||||
self.name,
|
||||
self.description,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class CircuitType(OrganizationalModel):
|
||||
@ -170,8 +143,6 @@ class CircuitType(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -181,13 +152,6 @@ class CircuitType(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Circuit(PrimaryModel):
|
||||
@ -259,9 +223,6 @@ class Circuit(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
]
|
||||
@ -276,19 +237,6 @@ class Circuit(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.cid,
|
||||
self.provider.name,
|
||||
self.type.name,
|
||||
self.get_status_display(),
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.install_date,
|
||||
self.commit_rate,
|
||||
self.description,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return CircuitStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
|
@ -264,7 +264,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
'termination_a': circuit.termination_a,
|
||||
'termination_z': circuit.termination_z,
|
||||
'form': form,
|
||||
'panel_class': 'default',
|
||||
'panel_class': 'light',
|
||||
'button_class': 'primary',
|
||||
'return_url': circuit.get_absolute_url(),
|
||||
})
|
||||
|
@ -101,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'device_count']
|
||||
|
||||
|
||||
class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
@ -136,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
|
||||
|
||||
|
||||
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
@ -232,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
|
@ -25,6 +25,7 @@ from .nested_serializers import *
|
||||
class CableTerminationSerializer(serializers.ModelSerializer):
|
||||
cable_peer_type = serializers.SerializerMethodField(read_only=True)
|
||||
cable_peer = serializers.SerializerMethodField(read_only=True)
|
||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_cable_peer_type(self, obj):
|
||||
if obj._cable_peer is not None:
|
||||
@ -42,6 +43,10 @@ class CableTerminationSerializer(serializers.ModelSerializer):
|
||||
return serializer(obj._cable_peer, context=context).data
|
||||
return None
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||
def get__occupied(self, obj):
|
||||
return obj._occupied
|
||||
|
||||
|
||||
class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
||||
@ -172,10 +177,9 @@ class RackSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'powerfeed_count',
|
||||
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
@ -284,9 +288,9 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height',
|
||||
'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count',
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count',
|
||||
]
|
||||
|
||||
|
||||
@ -385,8 +389,8 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@ -399,7 +403,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@ -465,10 +469,10 @@ class DeviceSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@ -501,10 +505,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@ -666,8 +670,9 @@ class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
@ -692,9 +697,9 @@ class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
@ -836,31 +841,6 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
return ret
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
interface_a = serializers.SerializerMethodField()
|
||||
interface_b = NestedInterfaceSerializer(source='_path.destination')
|
||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['interface_a', 'interface_b', 'connected_endpoint_reachable']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer)
|
||||
def get_interface_a(self, obj):
|
||||
context = {'request': self.context['request']}
|
||||
return NestedInterfaceSerializer(instance=obj, context=context).data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||
def get_connected_endpoint_reachable(self, obj):
|
||||
if obj._path is not None:
|
||||
return obj._path.is_active
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
@ -46,11 +46,6 @@ router.register('rear-ports', views.RearPortViewSet)
|
||||
router.register('device-bays', views.DeviceBayViewSet)
|
||||
router.register('inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Connections
|
||||
router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
|
||||
router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections')
|
||||
router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
|
||||
|
||||
# Cables
|
||||
router.register('cables', views.CableViewSet)
|
||||
|
||||
|
@ -570,38 +570,6 @@ class InventoryItemViewSet(ModelViewSet):
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filtersets.ConsoleConnectionFilterSet
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filtersets.PowerConnectionFilterSet
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.prefetch_related('device', '_path').filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
_path__destination_type__app_label='dcim',
|
||||
_path__destination_type__model='interface',
|
||||
_path__destination_id__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filterset_class = filtersets.InterfaceConnectionFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
@ -169,7 +169,7 @@ class DeviceStatusChoices(ChoiceSet):
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_STAGED: 'primary',
|
||||
STATUS_FAILED: 'danger',
|
||||
STATUS_INVENTORY: 'default',
|
||||
STATUS_INVENTORY: 'secondary',
|
||||
STATUS_DECOMMISSIONING: 'warning',
|
||||
}
|
||||
|
||||
@ -1066,14 +1066,21 @@ class CableStatusChoices(ChoiceSet):
|
||||
|
||||
class CableLengthUnitChoices(ChoiceSet):
|
||||
|
||||
# Metric
|
||||
UNIT_KILOMETER = 'km'
|
||||
UNIT_METER = 'm'
|
||||
UNIT_CENTIMETER = 'cm'
|
||||
|
||||
# Imperial
|
||||
UNIT_MILE = 'mi'
|
||||
UNIT_FOOT = 'ft'
|
||||
UNIT_INCH = 'in'
|
||||
|
||||
CHOICES = (
|
||||
(UNIT_KILOMETER, 'Kilometers'),
|
||||
(UNIT_METER, 'Meters'),
|
||||
(UNIT_CENTIMETER, 'Centimeters'),
|
||||
(UNIT_MILE, 'Miles'),
|
||||
(UNIT_FOOT, 'Feet'),
|
||||
(UNIT_INCH, 'Inches'),
|
||||
)
|
||||
|
@ -34,10 +34,11 @@ class RackElevationSVG:
|
||||
|
||||
@staticmethod
|
||||
def _get_device_description(device):
|
||||
return '{} ({}) — {} ({}U) {} {}'.format(
|
||||
return '{} ({}) — {} {} ({}U) {} {}'.format(
|
||||
device.name,
|
||||
device.device_role,
|
||||
device.device_type.display_name,
|
||||
device.device_type.manufacturer.name,
|
||||
device.device_type.model,
|
||||
device.device_type.u_height,
|
||||
device.asset_tag or '',
|
||||
device.serial or ''
|
||||
@ -64,7 +65,7 @@ class RackElevationSVG:
|
||||
drawing = svgwrite.Drawing(size=(width, height))
|
||||
|
||||
# add the stylesheet
|
||||
with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
|
||||
with open('{}/rack_elevation.css'.format(settings.STATIC_ROOT)) as css_file:
|
||||
drawing.defs.add(drawing.style(css_file.read()))
|
||||
|
||||
# add gradients
|
||||
|
@ -538,7 +538,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name', 'type', 'color']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
@ -549,7 +549,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'name', 'type', 'positions']
|
||||
fields = ['id', 'name', 'type', 'color', 'positions']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
@ -1027,7 +1027,7 @@ class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'name', 'label', 'type', 'description']
|
||||
fields = ['id', 'name', 'label', 'type', 'color', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
@ -1038,7 +1038,7 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'name', 'label', 'type', 'positions', 'description']
|
||||
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
|
@ -13,8 +13,8 @@ from timezone_field import TimeZoneFormField
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, Provider
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldModelCSVForm, CustomFieldFilterForm,
|
||||
CustomFieldModelForm, LocalConfigContextFilterForm,
|
||||
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelFilterForm, CustomFieldModelForm,
|
||||
CustomFieldsMixin, LocalConfigContextFilterForm,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||
@ -23,7 +23,7 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
ColorSelect, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField,
|
||||
ColorField, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
|
||||
NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
@ -54,14 +54,14 @@ def get_device_by_name_or_pk(name):
|
||||
return device
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
field_order = [
|
||||
'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id',
|
||||
'name', 'label', 'region_id', 'site_group_id', 'site_id',
|
||||
]
|
||||
field_groups = [
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -209,10 +209,10 @@ class RegionCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = Region.csv_headers
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -230,12 +230,8 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class RegionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = Site
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -266,10 +262,10 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = SiteGroup.csv_headers
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -287,12 +283,8 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = SiteGroup
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -391,7 +383,11 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = Site.csv_headers
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
)
|
||||
help_texts = {
|
||||
'time_zone': mark_safe(
|
||||
'Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)'
|
||||
@ -399,7 +395,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -444,13 +440,14 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
]
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Site
|
||||
field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_order = ['status', 'region_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['status', 'region_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['tag']
|
||||
]
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
@ -529,10 +526,10 @@ class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = Location.csv_headers
|
||||
fields = ('site', 'parent', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -557,12 +554,8 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = Location
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -606,21 +599,19 @@ class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = RackRole.csv_headers
|
||||
fields = ('name', 'slug', 'color', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6, # RGB color code
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
@ -739,7 +730,10 @@ class RackCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = Rack.csv_headers
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
@ -751,7 +745,7 @@ class RackCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -851,13 +845,14 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
]
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Rack
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_order = ['region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['status', 'role_id'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -913,7 +908,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
|
||||
class RackElevationFilterForm(RackFilterForm):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
|
||||
'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
|
||||
]
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@ -1044,7 +1039,7 @@ class RackReservationCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -1069,13 +1064,13 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = RackReservation
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_order = ['region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['user_id', 'tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -1124,10 +1119,10 @@ class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = Manufacturer.csv_headers
|
||||
fields = ('name', 'slug', 'description')
|
||||
|
||||
|
||||
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -1195,7 +1190,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -1218,12 +1213,15 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = DeviceType
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['manufacturer_id', 'subdevice_role'],
|
||||
['console_ports', 'console_server_ports'],
|
||||
['power_ports', 'power_outlets'],
|
||||
['interfaces', 'pass_through_ports'],
|
||||
['tag']
|
||||
]
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@ -1609,7 +1607,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1638,7 +1636,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'rear_port_set', 'description',
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1702,6 +1700,9 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -1715,7 +1716,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'positions', 'description',
|
||||
'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1728,13 +1729,18 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'positions', 'description')
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
|
||||
)
|
||||
|
||||
|
||||
class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@ -1751,6 +1757,9 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -1878,8 +1887,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -1929,21 +1937,19 @@ class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = DeviceRole.csv_headers
|
||||
fields = ('name', 'slug', 'color', 'vm_role', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6, # RGB color code
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
vm_role = forms.NullBooleanField(
|
||||
required=False,
|
||||
@ -1993,10 +1999,10 @@ class PlatformCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = Platform.csv_headers
|
||||
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
|
||||
|
||||
|
||||
class PlatformBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -2236,6 +2242,12 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||
choices=DeviceStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
virtual_chassis = CSVModelChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtual chassis'
|
||||
)
|
||||
cluster = CSVModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -2246,6 +2258,10 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
fields = []
|
||||
model = Device
|
||||
help_texts = {
|
||||
'vc_position': 'Virtual chassis position',
|
||||
'vc_priority': 'Virtual chassis priority',
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
@ -2284,7 +2300,8 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack', 'position', 'face', 'cluster', 'comments',
|
||||
'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster',
|
||||
'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -2319,7 +2336,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay', 'cluster', 'comments',
|
||||
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -2346,7 +2363,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
self.instance.rack = parent.rack
|
||||
|
||||
|
||||
class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -2402,16 +2419,19 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
]
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
|
||||
'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
|
||||
'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['region_id', 'site_id', 'location_id', 'rack_id'],
|
||||
['status', 'role_id', 'asset_tag'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['manufacturer_id', 'device_type_id'],
|
||||
['mac_address', 'has_primary_ip'],
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -2543,7 +2563,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldForm, ComponentForm):
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
@ -2560,7 +2580,7 @@ class ComponentCreateForm(BootstrapMixin, CustomFieldForm, ComponentForm):
|
||||
)
|
||||
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldForm, ComponentForm):
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -2582,6 +2602,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldForm, ComponentForm)
|
||||
|
||||
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
field_groups = [
|
||||
['name', 'label'],
|
||||
['type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
@ -2638,7 +2664,7 @@ class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsolePort.objects.all(),
|
||||
@ -2673,7 +2699,7 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ConsolePort.csv_headers
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
#
|
||||
@ -2683,6 +2709,12 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
field_groups = [
|
||||
['name', 'label'],
|
||||
['type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
@ -2739,7 +2771,7 @@ class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
@ -2774,7 +2806,7 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ConsoleServerPort.csv_headers
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
#
|
||||
@ -2784,6 +2816,11 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
field_groups = [
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False,
|
||||
@ -2844,7 +2881,7 @@ class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
@ -2872,7 +2909,9 @@ class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = PowerPort.csv_headers
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -2882,6 +2921,11 @@ class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
field_groups = [
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False,
|
||||
@ -2961,7 +3005,7 @@ class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
@ -3017,7 +3061,7 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = PowerOutlet.csv_headers
|
||||
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -3049,6 +3093,12 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
field_groups = [
|
||||
['name', 'label', 'type', 'enabled'],
|
||||
['mgmt_only', 'mac_address'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False,
|
||||
@ -3221,7 +3271,7 @@ class InterfaceBulkEditForm(
|
||||
]),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
@ -3351,7 +3401,10 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = Interface.csv_headers
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -3389,12 +3442,20 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
#
|
||||
|
||||
class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@ -3407,8 +3468,8 @@ class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'mark_connected', 'description',
|
||||
'tags',
|
||||
'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||
'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -3433,13 +3494,17 @@ class FrontPortCreateForm(ComponentCreateForm):
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'rear_port_set', 'mark_connected', 'description', 'tags',
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -3498,10 +3563,10 @@ class FrontPortCreateForm(ComponentCreateForm):
|
||||
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'mark_connected', 'description']),
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
@ -3529,7 +3594,10 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = FrontPort.csv_headers
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
)
|
||||
help_texts = {
|
||||
'rear_port_position': 'Mapped position on corresponding rear port',
|
||||
}
|
||||
@ -3563,11 +3631,19 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
field_groups = [
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@ -3580,7 +3656,7 @@ class RearPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'device', 'name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags',
|
||||
'device', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -3594,6 +3670,9 @@ class RearPortCreateForm(ComponentCreateForm):
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
@ -3601,12 +3680,13 @@ class RearPortCreateForm(ComponentCreateForm):
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags',
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
class RearPortBulkCreateForm(
|
||||
form_from_model(RearPort, ['type', 'positions', 'mark_connected']),
|
||||
form_from_model(RearPort, ['type', 'color', 'positions', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = RearPort
|
||||
@ -3614,10 +3694,10 @@ class RearPortBulkCreateForm(
|
||||
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'mark_connected', 'description']),
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
@ -3640,7 +3720,7 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = RearPort.csv_headers
|
||||
fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description')
|
||||
help_texts = {
|
||||
'positions': 'Number of front ports which may be mapped'
|
||||
}
|
||||
@ -3652,6 +3732,11 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
model = DeviceBay
|
||||
field_groups = [
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@ -3706,7 +3791,7 @@ class DeviceBayBulkEditForm(
|
||||
form_from_model(DeviceBay, ['label', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
@ -3734,7 +3819,7 @@ class DeviceBayCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = DeviceBay.csv_headers
|
||||
fields = ('device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -3840,7 +3925,9 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = InventoryItem.csv_headers
|
||||
fields = (
|
||||
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
)
|
||||
|
||||
|
||||
class InventoryItemBulkCreateForm(
|
||||
@ -3858,7 +3945,7 @@ class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldBulkEditForm
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
@ -3875,6 +3962,12 @@ class InventoryItemBulkEditForm(
|
||||
|
||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
model = InventoryItem
|
||||
field_groups = [
|
||||
['name', 'label', 'manufacturer_id'],
|
||||
['serial', 'asset_tag', 'discovered'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tag']
|
||||
]
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@ -4289,7 +4382,7 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
return length_unit if length_unit is not None else ''
|
||||
|
||||
|
||||
class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Cable.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -4310,10 +4403,8 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFo
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6, # RGB color code
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
length = forms.IntegerField(
|
||||
min_value=1,
|
||||
@ -4343,12 +4434,14 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFo
|
||||
})
|
||||
|
||||
|
||||
class CableFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = Cable
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['type', 'status', 'color'],
|
||||
['device_id', 'rack_id'],
|
||||
['region_id', 'site_id', 'tenant_id'],
|
||||
['tag']
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -4386,10 +4479,8 @@ class CableFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
choices=add_blank_choice(CableStatusChoices),
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6, # RGB color code
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@ -4619,6 +4710,10 @@ class DeviceVCMembershipForm(forms.ModelForm):
|
||||
# Require VC position (only required when the Device is a VirtualChassis member)
|
||||
self.fields['vc_position'].required = True
|
||||
|
||||
# Add bootstrap classes to form elements.
|
||||
self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
|
||||
self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
|
||||
|
||||
# Validation of vc_position is optional. This is only required when adding a new member to an existing
|
||||
# VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
|
||||
self.validate_vc_position = validate_vc_position
|
||||
@ -4688,7 +4783,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
||||
return device
|
||||
|
||||
|
||||
class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@ -4712,16 +4807,17 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = VirtualChassis.csv_headers
|
||||
fields = ('name', 'domain', 'master')
|
||||
|
||||
|
||||
class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = VirtualChassis
|
||||
field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_order = ['region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['tag']
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -4805,7 +4901,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = PowerPanel.csv_headers
|
||||
fields = ('site', 'location', 'name')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
@ -4817,7 +4913,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -4856,12 +4952,8 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
|
||||
nullable_fields = ['location']
|
||||
|
||||
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = PowerPanel
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -5006,7 +5098,10 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = PowerFeed.csv_headers
|
||||
fields = (
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
@ -5029,7 +5124,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@ -5090,12 +5185,15 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = PowerFeed
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['power_panel_id', 'rack_id'],
|
||||
['type', 'supply', 'max_utilization'],
|
||||
['phase', 'voltage', 'amperage'],
|
||||
['status', 'tag']
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
|
105
netbox/dcim/graphql/schema.py
Normal file
@ -0,0 +1,105 @@
|
||||
import graphene
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
|
||||
|
||||
class DCIMQuery(graphene.ObjectType):
|
||||
cable = ObjectField(CableType)
|
||||
cable_list = ObjectListField(CableType)
|
||||
|
||||
console_port = ObjectField(ConsolePortType)
|
||||
console_port_list = ObjectListField(ConsolePortType)
|
||||
|
||||
console_port_template = ObjectField(ConsolePortTemplateType)
|
||||
console_port_template_list = ObjectListField(ConsolePortTemplateType)
|
||||
|
||||
console_server_port = ObjectField(ConsoleServerPortType)
|
||||
console_server_port_list = ObjectListField(ConsoleServerPortType)
|
||||
|
||||
console_server_port_template = ObjectField(ConsoleServerPortTemplateType)
|
||||
console_server_port_template_list = ObjectListField(ConsoleServerPortTemplateType)
|
||||
|
||||
device = ObjectField(DeviceType)
|
||||
device_list = ObjectListField(DeviceType)
|
||||
|
||||
device_bay = ObjectField(DeviceBayType)
|
||||
device_bay_list = ObjectListField(DeviceBayType)
|
||||
|
||||
device_bay_template = ObjectField(DeviceBayTemplateType)
|
||||
device_bay_template_list = ObjectListField(DeviceBayTemplateType)
|
||||
|
||||
device_role = ObjectField(DeviceRoleType)
|
||||
device_role_list = ObjectListField(DeviceRoleType)
|
||||
|
||||
device_type = ObjectField(DeviceTypeType)
|
||||
device_type_list = ObjectListField(DeviceTypeType)
|
||||
|
||||
front_port = ObjectField(FrontPortType)
|
||||
front_port_list = ObjectListField(FrontPortType)
|
||||
|
||||
front_port_template = ObjectField(FrontPortTemplateType)
|
||||
front_port_template_list = ObjectListField(FrontPortTemplateType)
|
||||
|
||||
interface = ObjectField(InterfaceType)
|
||||
interface_list = ObjectListField(InterfaceType)
|
||||
|
||||
interface_template = ObjectField(InterfaceTemplateType)
|
||||
interface_template_list = ObjectListField(InterfaceTemplateType)
|
||||
|
||||
inventory_item = ObjectField(InventoryItemType)
|
||||
inventory_item_list = ObjectListField(InventoryItemType)
|
||||
|
||||
location = ObjectField(LocationType)
|
||||
location_list = ObjectListField(LocationType)
|
||||
|
||||
manufacturer = ObjectField(ManufacturerType)
|
||||
manufacturer_list = ObjectListField(ManufacturerType)
|
||||
|
||||
platform = ObjectField(PlatformType)
|
||||
platform_list = ObjectListField(PlatformType)
|
||||
|
||||
power_feed = ObjectField(PowerFeedType)
|
||||
power_feed_list = ObjectListField(PowerFeedType)
|
||||
|
||||
power_outlet = ObjectField(PowerOutletType)
|
||||
power_outlet_list = ObjectListField(PowerOutletType)
|
||||
|
||||
power_outlet_template = ObjectField(PowerOutletTemplateType)
|
||||
power_outlet_template_list = ObjectListField(PowerOutletTemplateType)
|
||||
|
||||
power_panel = ObjectField(PowerPanelType)
|
||||
power_panel_list = ObjectListField(PowerPanelType)
|
||||
|
||||
power_port = ObjectField(PowerPortType)
|
||||
power_port_list = ObjectListField(PowerPortType)
|
||||
|
||||
power_port_template = ObjectField(PowerPortTemplateType)
|
||||
power_port_template_list = ObjectListField(PowerPortTemplateType)
|
||||
|
||||
rack = ObjectField(RackType)
|
||||
rack_list = ObjectListField(RackType)
|
||||
|
||||
rack_reservation = ObjectField(RackReservationType)
|
||||
rack_reservation_list = ObjectListField(RackReservationType)
|
||||
|
||||
rack_role = ObjectField(RackRoleType)
|
||||
rack_role_list = ObjectListField(RackRoleType)
|
||||
|
||||
rear_port = ObjectField(RearPortType)
|
||||
rear_port_list = ObjectListField(RearPortType)
|
||||
|
||||
rear_port_template = ObjectField(RearPortTemplateType)
|
||||
rear_port_template_list = ObjectListField(RearPortTemplateType)
|
||||
|
||||
region = ObjectField(RegionType)
|
||||
region_list = ObjectListField(RegionType)
|
||||
|
||||
site = ObjectField(SiteType)
|
||||
site_list = ObjectListField(SiteType)
|
||||
|
||||
site_group = ObjectField(SiteGroupType)
|
||||
site_group_list = ObjectListField(SiteGroupType)
|
||||
|
||||
virtual_chassis = ObjectField(VirtualChassisType)
|
||||
virtual_chassis_list = ObjectListField(VirtualChassisType)
|
353
netbox/dcim/graphql/types.py
Normal file
@ -0,0 +1,353 @@
|
||||
from dcim import filtersets, models
|
||||
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType
|
||||
|
||||
__all__ = (
|
||||
'CableType',
|
||||
'ConsolePortType',
|
||||
'ConsolePortTemplateType',
|
||||
'ConsoleServerPortType',
|
||||
'ConsoleServerPortTemplateType',
|
||||
'DeviceType',
|
||||
'DeviceBayType',
|
||||
'DeviceBayTemplateType',
|
||||
'DeviceRoleType',
|
||||
'DeviceTypeType',
|
||||
'FrontPortType',
|
||||
'FrontPortTemplateType',
|
||||
'InterfaceType',
|
||||
'InterfaceTemplateType',
|
||||
'InventoryItemType',
|
||||
'LocationType',
|
||||
'ManufacturerType',
|
||||
'PlatformType',
|
||||
'PowerFeedType',
|
||||
'PowerOutletType',
|
||||
'PowerOutletTemplateType',
|
||||
'PowerPanelType',
|
||||
'PowerPortType',
|
||||
'PowerPortTemplateType',
|
||||
'RackType',
|
||||
'RackReservationType',
|
||||
'RackRoleType',
|
||||
'RearPortType',
|
||||
'RearPortTemplateType',
|
||||
'RegionType',
|
||||
'SiteType',
|
||||
'SiteGroupType',
|
||||
'VirtualChassisType',
|
||||
)
|
||||
|
||||
|
||||
class CableType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CableFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
def resolve_length_unit(self, info):
|
||||
return self.length_unit or None
|
||||
|
||||
|
||||
class ConsolePortType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.ConsolePortFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class ConsolePortTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePortTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class ConsoleServerPortType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPortTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class DeviceType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DeviceFilterSet
|
||||
|
||||
def resolve_face(self, info):
|
||||
return self.face or None
|
||||
|
||||
|
||||
class DeviceBayType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DeviceBayFilterSet
|
||||
|
||||
|
||||
class DeviceBayTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBayTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceRoleType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DeviceRoleFilterSet
|
||||
|
||||
|
||||
class DeviceTypeType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.DeviceTypeFilterSet
|
||||
|
||||
def resolve_subdevice_role(self, info):
|
||||
return self.subdevice_role or None
|
||||
|
||||
|
||||
class FrontPortType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.FrontPortFilterSet
|
||||
|
||||
|
||||
class FrontPortTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPortTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||
|
||||
|
||||
class InterfaceType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
|
||||
def resolve_mode(self, info):
|
||||
return self.mode or None
|
||||
|
||||
|
||||
class InterfaceTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
|
||||
class InventoryItemType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
|
||||
|
||||
class LocationType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
|
||||
class ManufacturerType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ManufacturerFilterSet
|
||||
|
||||
|
||||
class PlatformType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.PlatformFilterSet
|
||||
|
||||
|
||||
class PowerFeedType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.PowerFeedFilterSet
|
||||
|
||||
|
||||
class PowerOutletType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.PowerOutletFilterSet
|
||||
|
||||
def resolve_feed_leg(self, info):
|
||||
return self.feed_leg or None
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class PowerOutletTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutletTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||
|
||||
def resolve_feed_leg(self, info):
|
||||
return self.feed_leg or None
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class PowerPanelType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.PowerPanelFilterSet
|
||||
|
||||
|
||||
class PowerPortType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
exclude = ('_path',)
|
||||
filterset_class = filtersets.PowerPortFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class PowerPortTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPortTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class RackType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RackFilterSet
|
||||
|
||||
def resolve_type(self, info):
|
||||
return self.type or None
|
||||
|
||||
def resolve_outer_unit(self, info):
|
||||
return self.outer_unit or None
|
||||
|
||||
|
||||
class RackReservationType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RackReservationFilterSet
|
||||
|
||||
|
||||
class RackRoleType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RackRoleFilterSet
|
||||
|
||||
|
||||
class RearPortType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RearPortFilterSet
|
||||
|
||||
|
||||
class RearPortTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPortTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||
|
||||
|
||||
class RegionType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
|
||||
class SiteType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Site
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.SiteFilterSet
|
||||
|
||||
|
||||
class SiteGroupType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
|
||||
class VirtualChassisType(TaggedObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VirtualChassisFilterSet
|
16
netbox/dcim/migrations/0132_cable_length.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0131_consoleport_speed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||
),
|
||||
]
|
32
netbox/dcim/migrations/0133_port_colors.py
Normal file
@ -0,0 +1,32 @@
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0132_cable_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
]
|
@ -25,6 +25,7 @@ __all__ = (
|
||||
'Interface',
|
||||
'InterfaceTemplate',
|
||||
'InventoryItem',
|
||||
'Location',
|
||||
'Manufacturer',
|
||||
'Platform',
|
||||
'PowerFeed',
|
||||
@ -34,7 +35,6 @@ __all__ = (
|
||||
'PowerPort',
|
||||
'PowerPortTemplate',
|
||||
'Rack',
|
||||
'Location',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RearPort',
|
||||
|
@ -74,7 +74,9 @@ class Cable(PrimaryModel):
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
length = models.PositiveSmallIntegerField(
|
||||
length = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
@ -109,11 +111,6 @@ class Cable(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['pk']
|
||||
unique_together = (
|
||||
@ -287,20 +284,6 @@ class Cable(PrimaryModel):
|
||||
# Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
|
||||
self._pk = self.pk
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
'{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),
|
||||
self.termination_a_id,
|
||||
'{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model),
|
||||
self.termination_b_id,
|
||||
self.get_type_display(),
|
||||
self.get_status_display(),
|
||||
self.label,
|
||||
self.color,
|
||||
self.length,
|
||||
self.length_unit,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return CableStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
|
@ -6,7 +6,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.ordering import naturalize_interface
|
||||
from .device_components import (
|
||||
@ -267,6 +267,9 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
@ -290,19 +293,24 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
"Rear port ({}) must belong to the same device type".format(self.rear_port)
|
||||
)
|
||||
try:
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
"Rear port ({}) must belong to the same device type".format(self.rear_port)
|
||||
)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
||||
)
|
||||
)
|
||||
|
||||
except RearPortTemplate.DoesNotExist:
|
||||
pass
|
||||
|
||||
def instantiate(self, device):
|
||||
if self.rear_port:
|
||||
@ -314,6 +322,7 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=self.rear_port_position
|
||||
)
|
||||
@ -328,6 +337,9 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
@ -346,6 +358,7 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
name=self.name,
|
||||
label=self.label,
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
positions=self.positions
|
||||
)
|
||||
|
||||
|
@ -12,7 +12,7 @@ from dcim.constants import *
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@ -229,8 +229,6 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -238,17 +236,6 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
@ -272,8 +259,6 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -281,17 +266,6 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
@ -321,10 +295,6 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -332,18 +302,6 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.maximum_draw,
|
||||
self.allocated_draw,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -433,8 +391,6 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -442,18 +398,6 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.power_port.name if self.power_port else None,
|
||||
self.get_feed_leg_display(),
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -570,11 +514,6 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
related_query_name='interface'
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
unique_together = ('device', 'name')
|
||||
@ -582,23 +521,6 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:interface', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier if self.device else None,
|
||||
self.name,
|
||||
self.label,
|
||||
self.parent.name if self.parent else None,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_type_display(),
|
||||
self.enabled,
|
||||
self.mark_connected,
|
||||
self.mac_address,
|
||||
self.mtu,
|
||||
self.mgmt_only,
|
||||
self.description,
|
||||
self.get_mode_display(),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -692,6 +614,9 @@ class FrontPort(ComponentModel, CableTermination):
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPort',
|
||||
on_delete=models.CASCADE,
|
||||
@ -705,10 +630,6 @@ class FrontPort(ComponentModel, CableTermination):
|
||||
]
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = (
|
||||
@ -719,18 +640,6 @@ class FrontPort(ComponentModel, CableTermination):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:frontport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.rear_port.name,
|
||||
self.rear_port_position,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -757,6 +666,9 @@ class RearPort(ComponentModel, CableTermination):
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
)
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[
|
||||
@ -765,8 +677,6 @@ class RearPort(ComponentModel, CableTermination):
|
||||
]
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -785,17 +695,6 @@ class RearPort(ComponentModel, CableTermination):
|
||||
f"({frontport_count})"
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.positions,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
@ -814,8 +713,6 @@ class DeviceBay(ComponentModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
@ -823,15 +720,6 @@ class DeviceBay(ComponentModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicebay', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.label,
|
||||
self.installed_device.identifier if self.installed_device else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -907,26 +795,9 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device__id', 'parent__id', '_name')
|
||||
unique_together = ('device', 'parent', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.name or '{{{}}}'.format(self.device.pk),
|
||||
self.name,
|
||||
self.label,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.part_id,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.discovered,
|
||||
self.description,
|
||||
)
|
||||
|
@ -56,8 +56,6 @@ class Manufacturer(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -67,13 +65,6 @@ class Manufacturer(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:manufacturer', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class DeviceType(PrimaryModel):
|
||||
@ -336,10 +327,6 @@ class DeviceType(PrimaryModel):
|
||||
if self.rear_image:
|
||||
self.rear_image.delete(save=False)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f'{self.manufacturer.name} {self.model}'
|
||||
|
||||
@property
|
||||
def is_parent_device(self):
|
||||
return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
|
||||
@ -383,8 +370,6 @@ class DeviceRole(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -394,15 +379,6 @@ class DeviceRole(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicerole', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.color,
|
||||
self.vm_role,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Platform(OrganizationalModel):
|
||||
@ -446,8 +422,6 @@ class Platform(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -457,16 +431,6 @@ class Platform(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:platform', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.napalm_driver,
|
||||
self.napalm_args,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Device(PrimaryModel, ConfigContextModel):
|
||||
@ -612,19 +576,9 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
secrets = GenericRelation(
|
||||
to='secrets.Secret',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='device'
|
||||
)
|
||||
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack_name', 'position', 'face', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster',
|
||||
]
|
||||
@ -638,7 +592,13 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super().__str__()
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
elif self.device_type:
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
return super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
@ -820,36 +780,6 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
device.rack = self.rack
|
||||
device.save()
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name or '',
|
||||
self.device_role.name,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.device_type.manufacturer.name,
|
||||
self.device_type.model,
|
||||
self.platform.name if self.platform else None,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
elif self.device_type:
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
else:
|
||||
return '' # Device has not yet been created
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
@ -944,8 +874,6 @@ class VirtualChassis(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'domain', 'master']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
@ -982,10 +910,3 @@ class VirtualChassis(PrimaryModel):
|
||||
)
|
||||
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.domain,
|
||||
self.master.name if self.master else None,
|
||||
)
|
||||
|
@ -42,8 +42,6 @@ class PowerPanel(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'location', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = ['site', 'name']
|
||||
@ -54,13 +52,6 @@ class PowerPanel(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerpanel', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@ -133,10 +124,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization', 'available_power',
|
||||
@ -152,24 +139,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerfeed', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.power_panel.site.name,
|
||||
self.power_panel.name,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.name,
|
||||
self.get_status_display(),
|
||||
self.get_type_display(),
|
||||
self.mark_connected,
|
||||
self.get_supply_display(),
|
||||
self.get_phase_display(),
|
||||
self.voltage,
|
||||
self.amperage,
|
||||
self.max_utilization,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -58,8 +58,6 @@ class RackRole(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -69,14 +67,6 @@ class RackRole(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackrole', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.color,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Rack(PrimaryModel):
|
||||
@ -191,10 +181,6 @@ class Rack(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit',
|
||||
@ -209,7 +195,9 @@ class Rack(PrimaryModel):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super().__str__()
|
||||
if self.facility_id:
|
||||
return f'{self.name} ({self.facility_id})'
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
@ -249,27 +237,6 @@ class Rack(PrimaryModel):
|
||||
'location': f"Location must be from the same site, {self.site}."
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
self.facility_id,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else None,
|
||||
self.get_type_display() if self.type else None,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.width,
|
||||
self.u_height,
|
||||
self.desc_units,
|
||||
self.outer_width,
|
||||
self.outer_depth,
|
||||
self.outer_unit,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
if self.desc_units:
|
||||
@ -277,12 +244,6 @@ class Rack(PrimaryModel):
|
||||
else:
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.facility_id:
|
||||
return f'{self.name} ({self.facility_id})'
|
||||
return self.name
|
||||
|
||||
def get_status_class(self):
|
||||
return RackStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
@ -497,8 +458,6 @@ class RackReservation(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'location', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
|
||||
@ -535,17 +494,6 @@ class RackReservation(PrimaryModel):
|
||||
)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.rack.site.name,
|
||||
self.rack.location if self.rack.location else None,
|
||||
self.rack.name,
|
||||
','.join([str(u) for u in self.units]),
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.user.username,
|
||||
self.description
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_list(self):
|
||||
return array_to_string(self.units)
|
||||
|
@ -54,19 +54,9 @@ class Region(NestedGroupModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:region', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(region=self) |
|
||||
@ -106,19 +96,9 @@ class SiteGroup(NestedGroupModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:sitegroup', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(group=self) |
|
||||
@ -236,11 +216,6 @@ class Site(PrimaryModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
|
||||
@ -255,28 +230,6 @@ class Site(PrimaryModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:site', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.get_status_display(),
|
||||
self.region.name if self.region else None,
|
||||
self.group.name if self.group else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.facility,
|
||||
self.asn,
|
||||
self.time_zone,
|
||||
self.description,
|
||||
self.physical_address,
|
||||
self.shipping_address,
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
self.contact_name,
|
||||
self.contact_phone,
|
||||
self.contact_email,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return SiteStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
@ -318,7 +271,6 @@ class Location(NestedGroupModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
clone_fields = ['site', 'parent', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -331,15 +283,6 @@ class Location(NestedGroupModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
from cacheops import invalidate_obj
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||
from django.db import transaction
|
||||
@ -33,7 +32,6 @@ def rebuild_paths(obj):
|
||||
for cp in cable_paths:
|
||||
cp.delete()
|
||||
if cp.origin:
|
||||
invalidate_obj(cp.origin)
|
||||
create_cablepath(cp.origin)
|
||||
|
||||
|
||||
|
@ -52,10 +52,20 @@ def get_cabletermination_row_class(record):
|
||||
return ''
|
||||
|
||||
|
||||
def get_interface_state_attribute(record):
|
||||
"""
|
||||
Get interface enabled state as string to attach to <tr/> DOM element.
|
||||
"""
|
||||
if record.enabled:
|
||||
return "enabled"
|
||||
else:
|
||||
return "disabled"
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@ -528,6 +538,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class,
|
||||
'data-name': lambda record: record.name,
|
||||
'data-enabled': get_interface_state_attribute,
|
||||
}
|
||||
|
||||
|
||||
@ -538,6 +549,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
color = ColorColumn()
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
@ -551,10 +563,12 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected',
|
||||
'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
|
||||
|
||||
class DeviceFrontPortTable(FrontPortTable):
|
||||
@ -592,6 +606,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
color = ColorColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rearport_list'
|
||||
)
|
||||
@ -599,10 +614,10 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'color', 'description')
|
||||
|
||||
|
||||
class DeviceRearPortTable(RearPortTable):
|
||||
|
@ -4,7 +4,9 @@ from dcim.models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
||||
Manufacturer, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
)
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, LinkedCountColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortTemplateTable',
|
||||
@ -164,6 +166,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=FrontPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
@ -172,11 +175,12 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPortTemplateTable(ComponentTemplateTable):
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=RearPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
@ -185,7 +189,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
|
@ -26,17 +26,17 @@ CABLE_TERMINATION_PARENT = """
|
||||
|
||||
DEVICE_LINK = """
|
||||
<a href="{% url 'dcim:device' pk=record.pk %}">
|
||||
{{ record.name|default:'<span class="label label-info">Unnamed device</span>' }}
|
||||
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}
|
||||
</a>
|
||||
"""
|
||||
|
||||
DEVICEBAY_STATUS = """
|
||||
{% if record.installed_device_id %}
|
||||
<span class="label label-{{ record.installed_device.get_status_class }}">
|
||||
<span class="badge bg-{{ record.installed_device.get_status_class }}">
|
||||
{{ record.installed_device.get_status_display }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="label label-default">Vacant</span>
|
||||
<span class="badge bg-secondary">Vacant</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -60,7 +60,7 @@ INTERFACE_TAGGED_VLANS = """
|
||||
|
||||
POWERFEED_CABLE = """
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
"""
|
||||
@ -72,7 +72,7 @@ POWERFEED_CABLETERMINATION = """
|
||||
"""
|
||||
|
||||
LOCATION_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-xs 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>
|
||||
</a>
|
||||
"""
|
||||
@ -83,205 +83,199 @@ LOCATION_ELEVATIONS = """
|
||||
|
||||
CONSOLEPORT_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CONSOLESERVERPORT_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
POWERPORT_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-outlet' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
|
||||
<li><a href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-feed' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-outlet' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-feed' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
POWEROUTLET_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=record.pk termination_b_type='power-port' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-xs">
|
||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=record.pk termination_b_type='power-port' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
INTERFACE_BUTTONS = """
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-xs btn-success" title="Add IP address">
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-sm btn-success" title="Add IP address">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif record.is_connectable and perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
FRONTPORT_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
REARPORT_BUTTONS = """
|
||||
{% if record.cable %}
|
||||
<a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
<a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||
{% if not record.mark_connected %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-ethernet-cable" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
"""
|
||||
@ -289,11 +283,11 @@ REARPORT_BUTTONS = """
|
||||
DEVICEBAY_BUTTONS = """
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
{% if record.installed_device %}
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-xs">
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-minus-thick" aria-hidden="true" title="Remove device"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:devicebay_populate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-success btn-xs">
|
||||
<a href="{% url 'dcim:devicebay_populate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-success btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true" title="Install device"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -251,7 +251,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Rack
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['device_count', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@ -418,7 +418,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceType
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
brief_fields = ['device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'part_number': 'ABC123',
|
||||
}
|
||||
@ -866,7 +866,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Device
|
||||
brief_fields = ['display', 'display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'failed',
|
||||
}
|
||||
@ -1211,8 +1211,9 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Interface 6',
|
||||
'type': '1000base-t',
|
||||
'type': 'virtual',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'parent': interfaces[0].pk,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ from dcim.filtersets import *
|
||||
from dcim.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
@ -959,9 +960,9 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||
|
||||
FrontPortTemplate.objects.bulk_create((
|
||||
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH),
|
||||
FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC),
|
||||
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED),
|
||||
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN),
|
||||
FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE),
|
||||
))
|
||||
|
||||
def test_name(self):
|
||||
@ -977,6 +978,10 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_color(self):
|
||||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
@ -995,9 +1000,9 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
DeviceType.objects.bulk_create(device_types)
|
||||
|
||||
RearPortTemplate.objects.bulk_create((
|
||||
RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1),
|
||||
RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2),
|
||||
RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3),
|
||||
RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, positions=1),
|
||||
RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, positions=2),
|
||||
RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, positions=3),
|
||||
))
|
||||
|
||||
def test_name(self):
|
||||
@ -1013,6 +1018,10 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_color(self):
|
||||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_positions(self):
|
||||
params = {'positions': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -2153,9 +2162,9 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
RearPort.objects.bulk_create(rear_ports)
|
||||
|
||||
front_ports = (
|
||||
FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
|
||||
FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
|
||||
FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
|
||||
FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, rear_port=rear_ports[0], rear_port_position=1, description='First'),
|
||||
FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
|
||||
FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
|
||||
FrontPort(device=devices[3], name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
|
||||
FrontPort(device=devices[3], name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
|
||||
@ -2179,6 +2188,10 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_color(self):
|
||||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -2260,9 +2273,9 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
rear_ports = (
|
||||
RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
|
||||
RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
|
||||
RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
|
||||
RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, positions=1, description='First'),
|
||||
RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, positions=2, description='Second'),
|
||||
RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, positions=3, description='Third'),
|
||||
RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
|
||||
RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
|
||||
RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
|
||||
@ -2286,6 +2299,10 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_color(self):
|
||||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_positions(self):
|
||||
params = {'positions': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -580,11 +580,11 @@ device-bays:
|
||||
db1 = DeviceBayTemplate.objects.first()
|
||||
self.assertEqual(db1.name, 'Device Bay 1')
|
||||
|
||||
def test_devicetype_export(self):
|
||||
|
||||
def test_export_objects(self):
|
||||
url = reverse('dcim:devicetype_list')
|
||||
self.add_permissions('dcim.view_devicetype')
|
||||
|
||||
# Test default YAML export
|
||||
response = self.client.get('{}?export'.format(url))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
|
||||
@ -592,6 +592,11 @@ device-bays:
|
||||
self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
|
||||
self.assertEqual(data[0]['model'], 'Device Type 1')
|
||||
|
||||
# Test table-based export
|
||||
response = self.client.get(f'{url}?export=table')
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
|
||||
|
||||
|
||||
#
|
||||
# DeviceType components
|
||||
@ -1023,6 +1028,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
VirtualChassis.objects.create(name='Virtual Chassis 1')
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_role': deviceroles[1].pk,
|
||||
@ -1048,10 +1055,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front",
|
||||
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face,virtual_chassis,vc_position,vc_priority",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front,Virtual Chassis 1,1,10",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front,Virtual Chassis 1,2,20",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front,Virtual Chassis 1,3,30",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch
|
||||
@ -16,7 +19,6 @@ from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJou
|
||||
from ipam.models import IPAddress, Prefix, Service, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
from secrets.models import Secret
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.permissions import get_permission_for_model
|
||||
@ -1292,9 +1294,6 @@ class DeviceView(generic.ObjectView):
|
||||
# Services
|
||||
services = Service.objects.restrict(request.user, 'view').filter(device=instance)
|
||||
|
||||
# Secrets
|
||||
secrets = Secret.objects.restrict(request.user, 'view').filter(device=instance)
|
||||
|
||||
# Find up to ten devices in the same site with the same functional role for quick reference.
|
||||
related_devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
site=instance.site, device_role=instance.device_role
|
||||
@ -1306,7 +1305,6 @@ class DeviceView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'vc_members': vc_members,
|
||||
'related_devices': related_devices,
|
||||
'active_tab': 'device',
|
||||
@ -1912,6 +1910,30 @@ class InterfaceCreateView(generic.ComponentCreateView):
|
||||
model_form = forms.InterfaceForm
|
||||
template_name = 'dcim/device_component_add.html'
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Override inherited post() method to handle request to assign newly created
|
||||
interface objects (first object) to an IP Address object.
|
||||
"""
|
||||
logger = logging.getLogger('netbox.dcim.views.InterfaceCreateView')
|
||||
form = self.form(request.POST, initial=request.GET)
|
||||
new_objs = self.validate_form(request, form)
|
||||
|
||||
if form.is_valid() and not form.errors:
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and request.user.has_perm('ipam.add_ipaddress'):
|
||||
first_obj = new_objs[0].pk
|
||||
return redirect(f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}')
|
||||
else:
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'component_type': self.queryset.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
|
||||
class InterfaceEditView(generic.ObjectEditView):
|
||||
queryset = Interface.objects.all()
|
||||
@ -2510,23 +2532,6 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['console_server', 'port', 'device', 'console_port', 'reachable'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
return '\n'.join(csv_data)
|
||||
|
||||
def extra_context(self):
|
||||
return {
|
||||
'title': 'Console Connections'
|
||||
@ -2540,23 +2545,6 @@ class PowerConnectionsListView(generic.ObjectListView):
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['pdu', 'outlet', 'device', 'power_port', 'reachable'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
return '\n'.join(csv_data)
|
||||
|
||||
def extra_context(self):
|
||||
return {
|
||||
'title': 'Power Connections'
|
||||
@ -2574,25 +2562,6 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join([
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'reachable'
|
||||
])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
return '\n'.join(csv_data)
|
||||
|
||||
def extra_context(self):
|
||||
return {
|
||||
'title': 'Interface Connections'
|
||||
|
@ -1,200 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField
|
||||
from utilities.utils import content_type_name
|
||||
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
payload_url = LaxURLField(
|
||||
label='URL'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
exclude = ()
|
||||
|
||||
|
||||
@admin.register(Webhook)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
||||
'ssl_verification',
|
||||
]
|
||||
list_filter = [
|
||||
'enabled', 'type_create', 'type_update', 'type_delete', 'content_types',
|
||||
]
|
||||
form = WebhookForm
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'content_types', 'enabled')
|
||||
}),
|
||||
('Events', {
|
||||
'fields': ('type_create', 'type_update', 'type_delete')
|
||||
}),
|
||||
('HTTP Request', {
|
||||
'fields': (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
),
|
||||
'classes': ('monospace',)
|
||||
}),
|
||||
('SSL', {
|
||||
'fields': ('ssl_verification', 'ca_file_path')
|
||||
})
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
return ', '.join([ct.name for ct in obj.content_types.all()])
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
exclude = []
|
||||
widgets = {
|
||||
'default': forms.TextInput(),
|
||||
'validation_regex': forms.Textarea(
|
||||
attrs={
|
||||
'cols': 80,
|
||||
'rows': 3,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
form = CustomFieldForm
|
||||
list_display = [
|
||||
'name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description',
|
||||
]
|
||||
list_filter = [
|
||||
'type', 'required', 'content_types',
|
||||
]
|
||||
fieldsets = (
|
||||
('Custom Field', {
|
||||
'fields': ('type', 'name', 'weight', 'label', 'description', 'required', 'default', 'filter_logic')
|
||||
}),
|
||||
('Assignment', {
|
||||
'description': 'A custom field must be assigned to one or more object types.',
|
||||
'fields': ('content_types',)
|
||||
}),
|
||||
('Validation Rules', {
|
||||
'fields': ('validation_minimum', 'validation_maximum', 'validation_regex'),
|
||||
'classes': ('monospace',)
|
||||
}),
|
||||
('Choices', {
|
||||
'description': 'A selection field must have two or more choices assigned to it.',
|
||||
'fields': ('choices',)
|
||||
})
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
ct_names = [content_type_name(ct) for ct in obj.content_types.all()]
|
||||
return mark_safe('<br/>'.join(ct_names))
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
exclude = []
|
||||
widgets = {
|
||||
'link_text': forms.Textarea,
|
||||
'link_url': forms.Textarea,
|
||||
}
|
||||
help_texts = {
|
||||
'weight': 'A numeric weight to influence the ordering of this link among its peers. Lower weights appear '
|
||||
'first in a list.',
|
||||
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
|
||||
'Links which render as empty text will not be displayed.',
|
||||
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
}
|
||||
|
||||
|
||||
@admin.register(CustomLink)
|
||||
class CustomLinkAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Custom Link', {
|
||||
'fields': ('content_type', 'name', 'group_name', 'weight', 'button_class', 'new_window')
|
||||
}),
|
||||
('Templates', {
|
||||
'fields': ('link_text', 'link_url'),
|
||||
'classes': ('monospace',)
|
||||
})
|
||||
)
|
||||
list_display = [
|
||||
'name', 'content_type', 'group_name', 'weight',
|
||||
]
|
||||
list_filter = [
|
||||
'content_type',
|
||||
]
|
||||
form = CustomLinkForm
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
|
||||
class ExportTemplateForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
exclude = []
|
||||
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Export Template', {
|
||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('template_code',),
|
||||
'classes': ('monospace',)
|
||||
})
|
||||
)
|
||||
list_display = [
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
]
|
||||
list_filter = [
|
||||
'content_type',
|
||||
]
|
||||
form = ExportTemplateForm
|
||||
from .models import JobResult
|
||||
|
||||
|
||||
#
|
||||
|
@ -453,12 +453,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
|
||||
class ContentTypeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
|
||||
display_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'url', 'display', 'app_label', 'model', 'display_name']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
||||
def get_display_name(self, obj):
|
||||
return obj.app_labeled_name
|
||||
fields = ['id', 'url', 'display', 'app_label', 'model']
|
||||
|
@ -5,4 +5,5 @@ class ExtrasConfig(AppConfig):
|
||||
name = "extras"
|
||||
|
||||
def ready(self):
|
||||
import extras.lookups
|
||||
import extras.signals
|
||||
|
@ -45,7 +45,7 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
|
||||
|
||||
class CustomLinkButtonClassChoices(ChoiceSet):
|
||||
|
||||
CLASS_DEFAULT = 'default'
|
||||
CLASS_DEFAULT = 'outline-dark'
|
||||
CLASS_PRIMARY = 'primary'
|
||||
CLASS_SUCCESS = 'success'
|
||||
CLASS_INFO = 'info'
|
||||
@ -106,7 +106,7 @@ class JournalEntryKindChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
KIND_INFO: 'default',
|
||||
KIND_INFO: 'info',
|
||||
KIND_SUCCESS: 'success',
|
||||
KIND_WARNING: 'warning',
|
||||
KIND_DANGER: 'danger',
|
||||
@ -134,7 +134,7 @@ class LogLevelChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
LOG_DEFAULT: 'default',
|
||||
LOG_DEFAULT: 'secondary',
|
||||
LOG_SUCCESS: 'success',
|
||||
LOG_INFO: 'info',
|
||||
LOG_WARNING: 'warning',
|
||||
|
@ -7,13 +7,14 @@ from django.utils.translation import gettext as _
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
|
||||
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField,
|
||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm,
|
||||
CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||
from .models import *
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
@ -21,58 +22,431 @@ from .utils import FeatureQuery
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldForm(forms.Form):
|
||||
class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')),
|
||||
('Assigned Models', ('content_types',)),
|
||||
('Behavior', ('filter_logic',)),
|
||||
('Values', ('default', 'choices')),
|
||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldCSVForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
help_text="One or more assigned object types"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = (
|
||||
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
|
||||
'weight',
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CustomField.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
required = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class CustomFieldFilterForm(BootstrapMixin, forms.Form):
|
||||
field_groups = [
|
||||
['type', 'content_types'],
|
||||
['weight', 'required'],
|
||||
]
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=CustomFieldTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
required = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
|
||||
('Templates', ('link_text', 'link_url')),
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkCSVForm(CSVModelForm):
|
||||
content_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links'),
|
||||
help_text="Assigned object type"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = (
|
||||
'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url',
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CustomLink.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
required=False
|
||||
)
|
||||
new_window = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
button_class = forms.ChoiceField(
|
||||
choices=CustomLinkButtonClassChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class CustomLinkFilterForm(BootstrapMixin, forms.Form):
|
||||
field_groups = [
|
||||
['content_type'],
|
||||
['weight', 'new_window'],
|
||||
]
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
new_window = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
|
||||
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Custom Link', ('name', 'content_type', 'description')),
|
||||
('Template', ('template_code',)),
|
||||
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateCSVForm(CSVModelForm):
|
||||
content_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('export_templates'),
|
||||
help_text="Assigned object type"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = (
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ExportTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
mime_type = forms.CharField(
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
file_extension = forms.CharField(
|
||||
max_length=15,
|
||||
required=False
|
||||
)
|
||||
as_attachment = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description', 'mime_type', 'file_extension']
|
||||
|
||||
|
||||
class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
|
||||
field_groups = [
|
||||
['content_type', 'mime_type'],
|
||||
['file_extension', 'as_attachment'],
|
||||
]
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
mime_type = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
file_extension = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
as_attachment = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Webhook', ('name', 'enabled')),
|
||||
('Assigned Models', ('content_types',)),
|
||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||
('HTTP Request', (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
)),
|
||||
('SSL', ('ssl_verification', 'ca_file_path')),
|
||||
)
|
||||
|
||||
|
||||
class WebhookCSVForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
help_text="One or more assigned object types"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = (
|
||||
'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||
'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
|
||||
'ca_file_path'
|
||||
)
|
||||
|
||||
|
||||
class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Webhook.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_create = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_update = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
type_delete = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
http_method = forms.ChoiceField(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
required=False
|
||||
)
|
||||
payload_url = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
ssl_verification = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
secret = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
ca_file_path = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['secret', 'ca_file_path']
|
||||
|
||||
|
||||
class WebhookFilterForm(BootstrapMixin, forms.Form):
|
||||
field_groups = [
|
||||
['content_types', 'http_method'],
|
||||
['enabled', 'type_create', 'type_update', 'type_delete'],
|
||||
]
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
http_method = forms.MultipleChoiceField(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_create = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_update = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
type_delete = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Custom field models
|
||||
#
|
||||
|
||||
class CustomFieldsMixin:
|
||||
"""
|
||||
Extend Form to include custom field support.
|
||||
"""
|
||||
model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if self.model is None:
|
||||
raise NotImplementedError("CustomFieldForm must specify a model class.")
|
||||
self.custom_fields = []
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Append relevant custom fields to the form instance
|
||||
obj_type = ContentType.objects.get_for_model(self.model)
|
||||
for cf in CustomField.objects.filter(content_types=obj_type):
|
||||
field_name = 'cf_{}'.format(cf.name)
|
||||
self.fields[field_name] = cf.to_form_field()
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields.append(field_name)
|
||||
|
||||
|
||||
class CustomFieldModelForm(forms.ModelForm):
|
||||
"""
|
||||
Extend ModelForm to include custom field support.
|
||||
Extend a Form to include custom field support.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
self.custom_fields = []
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._append_customfield_fields()
|
||||
|
||||
def _get_content_type(self):
|
||||
"""
|
||||
Return the ContentType of the form's model.
|
||||
"""
|
||||
if not hasattr(self, 'model'):
|
||||
raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field()
|
||||
|
||||
def _append_customfield_fields(self):
|
||||
"""
|
||||
Append form fields for all CustomFields assigned to this model.
|
||||
Append form fields for all CustomFields assigned to this object type.
|
||||
"""
|
||||
content_type = self._get_content_type()
|
||||
|
||||
# Append form fields; assign initial values if modifying and existing object
|
||||
for cf in CustomField.objects.filter(content_types=self.obj_type):
|
||||
field_name = 'cf_{}'.format(cf.name)
|
||||
if self.instance.pk:
|
||||
self.fields[field_name] = cf.to_form_field(set_initial=False)
|
||||
self.fields[field_name].initial = self.instance.custom_field_data.get(cf.name)
|
||||
else:
|
||||
self.fields[field_name] = cf.to_form_field()
|
||||
for customfield in CustomField.objects.filter(content_types=content_type):
|
||||
field_name = f'cf_{customfield.name}'
|
||||
self.fields[field_name] = self._get_form_field(customfield)
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields.append(field_name)
|
||||
|
||||
|
||||
class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm):
|
||||
"""
|
||||
Extend ModelForm to include custom field support.
|
||||
"""
|
||||
def _get_content_type(self):
|
||||
return ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
if self.instance.pk:
|
||||
form_field = customfield.to_form_field(set_initial=False)
|
||||
form_field.initial = self.instance.custom_field_data.get(customfield.name, None)
|
||||
return form_field
|
||||
|
||||
return customfield.to_form_field()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Save custom field data on instance
|
||||
@ -84,18 +458,11 @@ class CustomFieldModelForm(forms.ModelForm):
|
||||
|
||||
class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
||||
|
||||
def _append_customfield_fields(self):
|
||||
|
||||
# Append form fields
|
||||
for cf in CustomField.objects.filter(content_types=self.obj_type):
|
||||
field_name = 'cf_{}'.format(cf.name)
|
||||
self.fields[field_name] = cf.to_form_field(for_csv_import=True)
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields.append(field_name)
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(for_csv_import=True)
|
||||
|
||||
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
class CustomFieldModelBulkEditForm(BulkEditForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -114,7 +481,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
||||
self.custom_fields.append(cf.name)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(forms.Form):
|
||||
class CustomFieldModelFilterForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -153,7 +520,7 @@ class TagCSVForm(CSVModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = Tag.csv_headers
|
||||
fields = ('name', 'slug', 'color', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
@ -193,10 +560,8 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
queryset=Tag.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6,
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
@ -294,13 +659,15 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
|
||||
'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
|
||||
'tenant_group_id', 'tenant_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['device_type_id', 'role_id', 'platform_id'],
|
||||
['cluster_group_id', 'cluster_id'],
|
||||
['tenant_group_id', 'tenant_id', 'tag']
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@ -393,6 +760,12 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
|
||||
@ -422,10 +795,10 @@ class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
model = JournalEntry
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['created_before', 'created_after', 'created_by_id'],
|
||||
['assigned_object_type_id', 'kind']
|
||||
]
|
||||
created_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
@ -465,10 +838,10 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
|
||||
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
model = ObjectChange
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['time_before', 'time_after', 'action'],
|
||||
['user_id', 'changed_object_type_id'],
|
||||
]
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
|
30
netbox/extras/graphql/schema.py
Normal file
@ -0,0 +1,30 @@
|
||||
import graphene
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
|
||||
|
||||
class ExtrasQuery(graphene.ObjectType):
|
||||
config_context = ObjectField(ConfigContextType)
|
||||
config_context_list = ObjectListField(ConfigContextType)
|
||||
|
||||
custom_field = ObjectField(CustomFieldType)
|
||||
custom_field_list = ObjectListField(CustomFieldType)
|
||||
|
||||
custom_link = ObjectField(CustomLinkType)
|
||||
custom_link_list = ObjectListField(CustomLinkType)
|
||||
|
||||
export_template = ObjectField(ExportTemplateType)
|
||||
export_template_list = ObjectListField(ExportTemplateType)
|
||||
|
||||
image_attachment = ObjectField(ImageAttachmentType)
|
||||
image_attachment_list = ObjectListField(ImageAttachmentType)
|
||||
|
||||
journal_entry = ObjectField(JournalEntryType)
|
||||
journal_entry_list = ObjectListField(JournalEntryType)
|
||||
|
||||
tag = ObjectField(TagType)
|
||||
tag_list = ObjectListField(TagType)
|
||||
|
||||
webhook = ObjectField(WebhookType)
|
||||
webhook_list = ObjectListField(WebhookType)
|
77
netbox/extras/graphql/types.py
Normal file
@ -0,0 +1,77 @@
|
||||
from extras import filtersets, models
|
||||
from netbox.graphql.types import BaseObjectType
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextType',
|
||||
'CustomFieldType',
|
||||
'CustomLinkType',
|
||||
'ExportTemplateType',
|
||||
'ImageAttachmentType',
|
||||
'JournalEntryType',
|
||||
'TagType',
|
||||
'WebhookType',
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigContext
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ConfigContextFilterSet
|
||||
|
||||
|
||||
class CustomFieldType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
class CustomLinkType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomLinkFilterSet
|
||||
|
||||
|
||||
class ExportTemplateType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ExportTemplateFilterSet
|
||||
|
||||
|
||||
class ImageAttachmentType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ImageAttachment
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
class JournalEntryType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.JournalEntry
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.JournalEntryFilterSet
|
||||
|
||||
|
||||
class TagType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
exclude = ('extras_taggeditem_items',)
|
||||
filterset_class = filtersets.TagFilterSet
|
||||
|
||||
|
||||
class WebhookType(BaseObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.WebhookFilterSet
|
17
netbox/extras/lookups.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.db.models import CharField, Lookup
|
||||
|
||||
|
||||
class Empty(Lookup):
|
||||
"""
|
||||
Filter on whether a string is empty.
|
||||
"""
|
||||
lookup_name = 'empty'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
CharField.register_lookup(Empty)
|
87
netbox/extras/management/commands/housekeeping.py
Normal file
@ -0,0 +1,87 @@
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.utils import timezone
|
||||
from packaging import version
|
||||
|
||||
from extras.models import ObjectChange
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Perform nightly housekeeping tasks. (This command can be run at any time.)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
||||
self.stdout.write("[*] Clearing expired authentication sessions")
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(f"\tConfigured session engine: {settings.SESSION_ENGINE}")
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
try:
|
||||
engine.SessionStore.clear_expired()
|
||||
self.stdout.write("\tSessions cleared.", self.style.SUCCESS)
|
||||
except NotImplementedError:
|
||||
self.stdout.write(
|
||||
f"\tThe configured session engine ({settings.SESSION_ENGINE}) does not support "
|
||||
f"clearing sessions; skipping."
|
||||
)
|
||||
|
||||
# Delete expired ObjectRecords
|
||||
self.stdout.write("[*] Checking for expired changelog records")
|
||||
if settings.CHANGELOG_RETENTION:
|
||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(f"Retention period: {settings.CHANGELOG_RETENTION} days")
|
||||
self.stdout.write(f"\tCut-off time: {cutoff}")
|
||||
expired_records = ObjectChange.objects.filter(time__lt=cutoff).count()
|
||||
if expired_records:
|
||||
self.stdout.write(f"\tDeleting {expired_records} expired records... ", self.style.WARNING, ending="")
|
||||
self.stdout.flush()
|
||||
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
||||
self.stdout.write("Done.", self.style.WARNING)
|
||||
else:
|
||||
self.stdout.write("\tNo expired records found.")
|
||||
else:
|
||||
self.stdout.write(
|
||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {settings.CHANGELOG_RETENTION})"
|
||||
)
|
||||
|
||||
# Check for new releases (if enabled)
|
||||
self.stdout.write("[*] Checking for latest release")
|
||||
if settings.RELEASE_CHECK_URL:
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
}
|
||||
|
||||
try:
|
||||
self.stdout.write(f"\tFetching {settings.RELEASE_CHECK_URL}")
|
||||
response = requests.get(
|
||||
url=settings.RELEASE_CHECK_URL,
|
||||
headers=headers,
|
||||
proxies=settings.HTTP_PROXIES
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
releases = []
|
||||
for release in response.json():
|
||||
if 'tag_name' not in release or release.get('devrelease') or release.get('prerelease'):
|
||||
continue
|
||||
releases.append((version.parse(release['tag_name']), release.get('html_url')))
|
||||
latest_release = max(releases)
|
||||
self.stdout.write(f"\tFound {len(response.json())} releases; {len(releases)} usable")
|
||||
self.stdout.write(f"\tLatest release: {latest_release[0]}")
|
||||
|
||||
# Cache the most recent release
|
||||
cache.set('latest_release', latest_release, None)
|
||||
|
||||
except requests.exceptions.RequestException as exc:
|
||||
self.stdout.write(f"\tRequest error: {exc}")
|
||||
else:
|
||||
self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
|
||||
|
||||
self.stdout.write("Finished.", self.style.SUCCESS)
|
@ -9,7 +9,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization']
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
|
@ -1,4 +1,3 @@
|
||||
from cacheops import invalidate_model
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
@ -108,8 +107,5 @@ class Command(BaseCommand):
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS(str(count)))
|
||||
|
||||
# Invalidate cached queries
|
||||
invalidate_model(model)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
|
@ -52,7 +52,6 @@ class Migration(migrations.Migration):
|
||||
('circuits', '0015_custom_tag_models'),
|
||||
('dcim', '0070_custom_tag_models'),
|
||||
('ipam', '0025_custom_tag_models'),
|
||||
('secrets', '0006_custom_tag_models'),
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
('virtualization', '0009_custom_tag_models'),
|
||||
]
|
||||
|