diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 66f675d7c..97b55b285 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index eaa9a1200..bfb0cc7aa 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -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
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9182457a0..36d1f3350 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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/
diff --git a/.gitignore b/.gitignore
index 95e4ff702..66f4cb2ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
*.pyc
*.swp
+node_modules
+/netbox/project-static/.cache
/netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py
/netbox/reports/*
diff --git a/README.md b/README.md
index cb1991447..4fa3e7973 100644
--- a/README.md
+++ b/README.md
@@ -54,11 +54,13 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
### Screenshots
-
+
-
+
-
+
+
+
### Related projects
diff --git a/base_requirements.txt b/base_requirements.txt
index bf03bf71e..856439627 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -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
diff --git a/contrib/netbox-housekeeping.sh b/contrib/netbox-housekeeping.sh
new file mode 100644
index 000000000..5b1c46c5e
--- /dev/null
+++ b/contrib/netbox-housekeeping.sh
@@ -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
diff --git a/docs/additional-features/caching.md b/docs/additional-features/caching.md
deleted file mode 100644
index 18c9dca68..000000000
--- a/docs/additional-features/caching.md
+++ /dev/null
@@ -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
-```
diff --git a/docs/additional-features/webhooks.md b/docs/additional-features/webhooks.md
index f3dd80337..e3b352125 100644
--- a/docs/additional-features/webhooks.md
+++ b/docs/additional-features/webhooks.md
@@ -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
diff --git a/docs/administration/housekeeping.md b/docs/administration/housekeeping.md
new file mode 100644
index 000000000..c562613eb
--- /dev/null
+++ b/docs/administration/housekeeping.md
@@ -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.
diff --git a/docs/administration/netbox-shell.md b/docs/administration/netbox-shell.md
index 2a8d31494..eb73776e1 100644
--- a/docs/administration/netbox-shell.md
+++ b/docs/administration/netbox-shell.md
@@ -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})
```
diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md
index 927bf9f37..46c08bdc1 100644
--- a/docs/configuration/optional-settings.md
+++ b/docs/configuration/optional-settings.md
@@ -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.
---
diff --git a/docs/core-functionality/secrets.md b/docs/core-functionality/secrets.md
deleted file mode 100644
index 68771310c..000000000
--- a/docs/core-functionality/secrets.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Secrets
-
-{!docs/models/secrets/secret.md!}
-{!docs/models/secrets/secretrole.md!}
-
----
-
-{!docs/models/secrets/userkey.md!}
diff --git a/docs/additional-features/custom-fields.md b/docs/customization/custom-fields.md
similarity index 97%
rename from docs/additional-features/custom-fields.md
rename to docs/customization/custom-fields.md
index 649f69256..a9acfb3f7 100644
--- a/docs/additional-features/custom-fields.md
+++ b/docs/customization/custom-fields.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)
diff --git a/docs/additional-features/custom-links.md b/docs/customization/custom-links.md
similarity index 75%
rename from docs/additional-features/custom-links.md
rename to docs/customization/custom-links.md
index 0a00b6d68..dc8f28b71 100644
--- a/docs/additional-features/custom-links.md
+++ b/docs/customization/custom-links.md
@@ -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:
View NMS
```
-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
diff --git a/docs/additional-features/custom-scripts.md b/docs/customization/custom-scripts.md
similarity index 96%
rename from docs/additional-features/custom-scripts.md
rename to docs/customization/custom-scripts.md
index 8fe3661ed..a27bcab83 100644
--- a/docs/additional-features/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -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'
}
diff --git a/docs/customization/custom-validation.md b/docs/customization/custom-validation.md
new file mode 100644
index 000000000..720e8e487
--- /dev/null
+++ b/docs/customization/custom-validation.md
@@ -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,
+ }
+ }),
+ )
+}
+```
diff --git a/docs/additional-features/export-templates.md b/docs/customization/export-templates.md
similarity index 79%
rename from docs/additional-features/export-templates.md
rename to docs/customization/export-templates.md
index b3f585bee..0d0f7169e 100644
--- a/docs/additional-features/export-templates.md
+++ b/docs/customization/export-templates.md
@@ -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.
diff --git a/docs/additional-features/reports.md b/docs/customization/reports.md
similarity index 100%
rename from docs/additional-features/reports.md
rename to docs/customization/reports.md
diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md
index 9a27b68b6..994d2a040 100644
--- a/docs/development/extending-models.md
+++ b/docs/development/extending-models.md
@@ -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 `.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//.md`. Update this file to include any relevant information about the new field.
diff --git a/docs/development/index.md b/docs/development/index.md
index b856e315b..c10c752d5 100644
--- a/docs/development/index.md
+++ b/docs/development/index.md
@@ -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.)
diff --git a/docs/development/models.md b/docs/development/models.md
index 7dec2cb61..93a10fff6 100644
--- a/docs/development/models.md
+++ b/docs/development/models.md
@@ -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)
diff --git a/docs/development/signals.md b/docs/development/signals.md
new file mode 100644
index 000000000..8a5d8e43f
--- /dev/null
+++ b/docs/development/signals.md
@@ -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()`
diff --git a/docs/graphql-api/overview.md b/docs/graphql-api/overview.md
new file mode 100644
index 000000000..f1ce4f455
--- /dev/null
+++ b/docs/graphql-api/overview.md
@@ -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.
diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md
index 7a8e0bc80..9ffa19e3a 100644
--- a/docs/installation/3-netbox.md
+++ b/docs/installation/3-netbox.md
@@ -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:
diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md
index e824ad7ab..0bf356ae4 100644
--- a/docs/installation/upgrading.md
+++ b/docs/installation/upgrading.md
@@ -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.
diff --git a/docs/media/cable-dark.png b/docs/media/cable-dark.png
new file mode 100644
index 000000000..cd23d6306
Binary files /dev/null and b/docs/media/cable-dark.png differ
diff --git a/docs/media/cable-light.png b/docs/media/cable-light.png
new file mode 100644
index 000000000..a7f7f7875
Binary files /dev/null and b/docs/media/cable-light.png differ
diff --git a/docs/media/home-dark.png b/docs/media/home-dark.png
new file mode 100644
index 000000000..9da11df79
Binary files /dev/null and b/docs/media/home-dark.png differ
diff --git a/docs/media/home-light.png b/docs/media/home-light.png
new file mode 100644
index 000000000..25d255ebf
Binary files /dev/null and b/docs/media/home-light.png differ
diff --git a/docs/media/prefixes-dark.png b/docs/media/prefixes-dark.png
new file mode 100644
index 000000000..f4867764d
Binary files /dev/null and b/docs/media/prefixes-dark.png differ
diff --git a/docs/media/prefixes-light.png b/docs/media/prefixes-light.png
new file mode 100644
index 000000000..30189a27d
Binary files /dev/null and b/docs/media/prefixes-light.png differ
diff --git a/docs/media/rack-dark.png b/docs/media/rack-dark.png
new file mode 100644
index 000000000..a419f4c63
Binary files /dev/null and b/docs/media/rack-dark.png differ
diff --git a/docs/media/rack-light.png b/docs/media/rack-light.png
new file mode 100644
index 000000000..6308baf5b
Binary files /dev/null and b/docs/media/rack-light.png differ
diff --git a/docs/media/screenshot1.png b/docs/media/screenshot1.png
deleted file mode 100644
index e25a5ed93..000000000
Binary files a/docs/media/screenshot1.png and /dev/null differ
diff --git a/docs/media/screenshot2.png b/docs/media/screenshot2.png
deleted file mode 100644
index bce0a3f5d..000000000
Binary files a/docs/media/screenshot2.png and /dev/null differ
diff --git a/docs/media/screenshot3.png b/docs/media/screenshot3.png
deleted file mode 100644
index 6cf67d8f7..000000000
Binary files a/docs/media/screenshot3.png and /dev/null differ
diff --git a/docs/models/dcim/virtualchassis.md b/docs/models/dcim/virtualchassis.md
index b2a7d3bc9..3b6fb9d17 100644
--- a/docs/models/dcim/virtualchassis.md
+++ b/docs/models/dcim/virtualchassis.md
@@ -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.
diff --git a/docs/models/secrets/secret.md b/docs/models/secrets/secret.md
deleted file mode 100644
index 4c3482624..000000000
--- a/docs/models/secrets/secret.md
+++ /dev/null
@@ -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.
diff --git a/docs/models/secrets/secretrole.md b/docs/models/secrets/secretrole.md
deleted file mode 100644
index 23f68912b..000000000
--- a/docs/models/secrets/secretrole.md
+++ /dev/null
@@ -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
diff --git a/docs/models/secrets/userkey.md b/docs/models/secrets/userkey.md
deleted file mode 100644
index fd66c7bdb..000000000
--- a/docs/models/secrets/userkey.md
+++ /dev/null
@@ -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.
diff --git a/docs/plugins/development.md b/docs/plugins/development.md
index 7aab5ca8a..ed81eb891 100644
--- a/docs/plugins/development.md
+++ b/docs/plugins/development.md
@@ -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
-```
\ No newline at end of file
+```
diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md
index f7f6f36e9..247ba3e1d 120000
--- a/docs/release-notes/index.md
+++ b/docs/release-notes/index.md
@@ -1 +1 @@
-version-2.11.md
\ No newline at end of file
+version-3.0.md
\ No newline at end of file
diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md
index e1840319e..1ff9a8483 100644
--- a/docs/release-notes/version-2.11.md
+++ b/docs/release-notes/version-2.11.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
---
diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md
index e8228920d..7e9e8fea3 100644
--- a/docs/release-notes/version-2.6.md
+++ b/docs/release-notes/version-2.6.md
@@ -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.
diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md
new file mode 100644
index 000000000..73ed38d8d
--- /dev/null
+++ b/docs/release-notes/version-3.0.md
@@ -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
diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md
index 7fb789e0f..246896233 100644
--- a/docs/rest-api/authentication.md
+++ b/docs/rest-api/authentication.md
@@ -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": ""
+}
+```
diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md
index b77513297..471beffee 100644
--- a/docs/rest-api/filtering.md
+++ b/docs/rest-api/filtering.md
@@ -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
diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md
index 088286e22..6a9235438 100644
--- a/docs/rest-api/overview.md
+++ b/docs/rest-api/overview.md
@@ -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:///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:///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/`
diff --git a/docs/rest-api/working-with-secrets.md b/docs/rest-api/working-with-secrets.md
deleted file mode 100644
index 5fbbf7355..000000000
--- a/docs/rest-api/working-with-secrets.md
+++ /dev/null
@@ -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@"
-```
-
-```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.
diff --git a/docs/screenshots/index.md b/docs/screenshots/index.md
new file mode 100644
index 000000000..6178630bd
--- /dev/null
+++ b/docs/screenshots/index.md
@@ -0,0 +1,36 @@
+# Screenshots
+
+## Light Mode
+
+### Home Page
+
+
+
+### Rack Elevation
+
+
+
+### Prefixes
+
+
+
+### Cable Trace
+
+
+## Dark Mode
+
+### Home Page
+
+
+
+### Rack Elevation
+
+
+
+### Prefixes
+
+
+
+### Cable Trace
+
+
diff --git a/mkdocs.yml b/mkdocs.yml
index fb5cf1890..031fab634 100644
--- a/mkdocs.yml
+++ b/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'
diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py
index 1b3eb3242..751f37a64 100644
--- a/netbox/circuits/forms.py
+++ b/netbox/circuits/forms.py
@@ -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,
diff --git a/netbox/secrets/api/__init__.py b/netbox/circuits/graphql/__init__.py
similarity index 100%
rename from netbox/secrets/api/__init__.py
rename to netbox/circuits/graphql/__init__.py
diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py
new file mode 100644
index 000000000..f65874239
--- /dev/null
+++ b/netbox/circuits/graphql/schema.py
@@ -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)
diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py
new file mode 100644
index 000000000..bde25a09f
--- /dev/null
+++ b/netbox/circuits/graphql/types.py
@@ -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
diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py
index 699ded7b0..39f38d0b0 100644
--- a/netbox/circuits/models.py
+++ b/netbox/circuits/models.py
@@ -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)
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 15e6bed2f..b4bb0155e 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -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(),
})
diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index 80e003efc..67ae9b046 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -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):
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 377449140..1cdcc252f 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -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
#
diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py
index 43f956cb2..491f4e7f2 100644
--- a/netbox/dcim/api/urls.py
+++ b/netbox/dcim/api/urls.py
@@ -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)
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index 47ab26828..9592dabae 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -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
#
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 63f44ea37..c3e0a1a75 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -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'),
)
diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py
index 93c44f087..473b9608f 100644
--- a/netbox/dcim/elevations.py
+++ b/netbox/dcim/elevations.py
@@ -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
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index b04c14ba9..079e43007 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -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):
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index eec828f13..4fb2e71fb 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -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 (available options)'
@@ -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. 00ff00)'),
}
-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. 00ff00)'),
}
-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,
diff --git a/netbox/secrets/migrations/__init__.py b/netbox/dcim/graphql/__init__.py
similarity index 100%
rename from netbox/secrets/migrations/__init__.py
rename to netbox/dcim/graphql/__init__.py
diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py
new file mode 100644
index 000000000..13e0c20ec
--- /dev/null
+++ b/netbox/dcim/graphql/schema.py
@@ -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)
diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py
new file mode 100644
index 000000000..de091ec64
--- /dev/null
+++ b/netbox/dcim/graphql/types.py
@@ -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
diff --git a/netbox/dcim/migrations/0132_cable_length.py b/netbox/dcim/migrations/0132_cable_length.py
new file mode 100644
index 000000000..e20a8b8aa
--- /dev/null
+++ b/netbox/dcim/migrations/0132_cable_length.py
@@ -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),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0133_port_colors.py b/netbox/dcim/migrations/0133_port_colors.py
new file mode 100644
index 000000000..8cae7ac8e
--- /dev/null
+++ b/netbox/dcim/migrations/0133_port_colors.py
@@ -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),
+ ),
+ ]
diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py
index ee19d553d..0375a9fb4 100644
--- a/netbox/dcim/models/__init__.py
+++ b/netbox/dcim/models/__init__.py
@@ -25,6 +25,7 @@ __all__ = (
'Interface',
'InterfaceTemplate',
'InventoryItem',
+ 'Location',
'Manufacturer',
'Platform',
'PowerFeed',
@@ -34,7 +35,6 @@ __all__ = (
'PowerPort',
'PowerPortTemplate',
'Rack',
- 'Location',
'RackReservation',
'RackRole',
'RearPort',
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index e7040376c..c3f8cac3f 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -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)
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index d99d6b7c3..e704f74a7 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -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
)
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index bd7f4ac55..84a78c525 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -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,
- )
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 95c3c50db..10cd35c13 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -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,
- )
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 03e77eea9..f81abd328 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -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()
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index c4416ca28..3370badc3 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -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)
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index 7ab37567a..943e98106 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -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()
diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py
index 33a868f2c..9fc68ee70 100644
--- a/netbox/dcim/signals.py
+++ b/netbox/dcim/signals.py
@@ -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)
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 258e712d5..8ac53aee6 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -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