mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #12370 from netbox-community/feature
Prepare for v3.5.0 release
This commit is contained in:
commit
a2f4fce5b3
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.4.10
|
||||
placeholder: v3.5-beta2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.4.10
|
||||
placeholder: v3.5-beta2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -2,6 +2,10 @@
|
||||
# https://github.com/mozilla/bleach/blob/main/CHANGES
|
||||
bleach<6.0
|
||||
|
||||
# Python client for Amazon AWS API
|
||||
# https://github.com/boto/boto3/blob/develop/CHANGELOG.rst
|
||||
boto3
|
||||
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://docs.djangoproject.com/en/stable/releases/
|
||||
Django<4.2
|
||||
@ -62,9 +66,21 @@ django-timezone-field
|
||||
# https://www.django-rest-framework.org/community/release-notes/
|
||||
djangorestframework
|
||||
|
||||
# Swagger/OpenAPI schema generation for REST APIs
|
||||
# https://drf-yasg.readthedocs.io/en/stable/changelog.html
|
||||
drf-yasg[validation]
|
||||
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
|
||||
drf-spectacular
|
||||
|
||||
# Serve self-contained distribution builds of Swagger UI and Redoc with Django.
|
||||
# https://github.com/tfranzel/drf-spectacular-sidecar
|
||||
drf-spectacular-sidecar
|
||||
|
||||
# Git client for file sync
|
||||
# https://github.com/jelmer/dulwich/releases
|
||||
dulwich
|
||||
|
||||
# RSS feed parser
|
||||
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
|
||||
feedparser
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django/releases
|
||||
|
95605
contrib/openapi2.json
Normal file
95605
contrib/openapi2.json
Normal file
File diff suppressed because it is too large
Load Diff
69695
contrib/openapi2.yaml
Normal file
69695
contrib/openapi2.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
||||
|
||||
### Enabling Error Reporting
|
||||
|
||||
NetBox v3.2.3 and later support native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
|
||||
NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
|
||||
|
||||
```python
|
||||
SENTRY_ENABLED = True
|
||||
|
@ -4,7 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention)
|
||||
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention)
|
||||
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
|
||||
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Object-Based Permissions
|
||||
|
||||
NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
NetBox employs a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
|
||||
A permission in NetBox represents a relationship shared by several components:
|
||||
|
||||
@ -20,7 +20,7 @@ There are four core actions that can be permitted for each type of object within
|
||||
* **Change** - Modify an existing object
|
||||
* **Delete** - Delete an existing object
|
||||
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field.
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `run` permission for scripts allows a user to execute custom scripts. These can be specified when granting a permission in the "additional actions" field.
|
||||
|
||||
!!! note
|
||||
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
|
||||
@ -58,8 +58,6 @@ Additionally, where multiple permissions have been assigned for an object type,
|
||||
|
||||
### User Token
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.3"
|
||||
|
||||
When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as:
|
||||
|
||||
```json
|
||||
|
@ -1,5 +1,50 @@
|
||||
# Default Value Parameters
|
||||
|
||||
## DEFAULT_DASHBOARD
|
||||
|
||||
This parameter controls the content and layout of user's default dashboard. Once the dashboard has been created, the user is free to customize it as they please by adding, removing, and reconfiguring widgets.
|
||||
|
||||
This parameter must specify an iterable of dictionaries, each representing a discrete dashboard widget and its configuration. The follow widget attributes are supported:
|
||||
|
||||
* `widget`: Dotted path to the Python class (required)
|
||||
* `width`: Default widget width (between 1 and 12, inclusive)
|
||||
* `height`: Default widget height, in rows
|
||||
* `title`: Widget title
|
||||
* `color`: Color of the widget's title bar, specified by name
|
||||
* `config`: Dictionary mapping of any widget configuration parameters
|
||||
|
||||
A brief example configuration is provided below.
|
||||
|
||||
```python
|
||||
DEFAULT_DASHBOARD = [
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 2,
|
||||
'title': 'Organization',
|
||||
'config': {
|
||||
'models': [
|
||||
'dcim.site',
|
||||
'tenancy.tenant',
|
||||
'tenancy.contact',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'title': 'IPAM',
|
||||
'color': 'blue',
|
||||
'config': {
|
||||
'models': [
|
||||
'ipam.prefix',
|
||||
'ipam.iprange',
|
||||
'ipam.ipaddress',
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## DEFAULT_USER_PREFERENCES
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
@ -26,14 +26,10 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
||||
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
|
||||
* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique)
|
||||
* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled)
|
||||
* [`JOBRESULT_RETENTION`](./miscellaneous.md#jobresult_retention)
|
||||
* [`JOB_RETENTION`](./miscellaneous.md#job_retention)
|
||||
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
|
||||
* [`MAPS_URL`](./miscellaneous.md#maps_url)
|
||||
* [`MAX_PAGE_SIZE`](./miscellaneous.md#max_page_size)
|
||||
* [`NAPALM_ARGS`](./napalm.md#napalm_args)
|
||||
* [`NAPALM_PASSWORD`](./napalm.md#napalm_password)
|
||||
* [`NAPALM_TIMEOUT`](./napalm.md#napalm_timeout)
|
||||
* [`NAPALM_USERNAME`](./napalm.md#napalm_username)
|
||||
* [`PAGINATE_COUNT`](./default-values.md#paginate_count)
|
||||
* [`POWERFEED_DEFAULT_AMPERAGE`](./default-values.md#powerfeed_default_amperage)
|
||||
* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization)
|
||||
|
@ -97,14 +97,16 @@ Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## JOBRESULT_RETENTION
|
||||
## JOB_RETENTION
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
!!! note
|
||||
This parameter was renamed from `JOBRESULT_RETENTION` in NetBox v3.5.
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain
|
||||
job results in the database indefinitely.
|
||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain job results in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
@ -1,51 +0,0 @@
|
||||
# NAPALM Parameters
|
||||
|
||||
## NAPALM_USERNAME
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../integrations/napalm.md), if installed. Both parameters are optional.
|
||||
|
||||
!!! note
|
||||
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_ARGS
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
|
||||
|
||||
```python
|
||||
NAPALM_ARGS = {
|
||||
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
|
||||
'port': 2222,
|
||||
}
|
||||
```
|
||||
|
||||
Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
|
||||
|
||||
```python
|
||||
NAPALM_USERNAME = 'username'
|
||||
NAPALM_PASSWORD = 'MySecretPassword'
|
||||
NAPALM_ARGS = {
|
||||
'secret': NAPALM_PASSWORD,
|
||||
# Include any additional args here
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_TIMEOUT
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: 30 seconds
|
||||
|
||||
The amount of time (in seconds) to wait for NAPALM to connect to a device.
|
||||
|
||||
---
|
||||
|
@ -144,8 +144,6 @@ REDIS = {
|
||||
|
||||
## SECRET_KEY
|
||||
|
||||
This is a secret, random string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside of the configuration file. `SECRET_KEY` can be changed at any time, however be aware that doing so will invalidate all existing sessions.
|
||||
This is a secret, pseudorandom string used to assist in the creation new cryptographic hashes for passwords and HTTP cookies. The key defined here should not be shared outside the configuration file. `SECRET_KEY` can be changed at any time without impacting stored data, however be aware that doing so will invalidate all existing user sessions. NetBox deployments comprising multiple nodes must have the same secret key configured on all nodes.
|
||||
|
||||
Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.
|
||||
|
||||
`SECRET_KEY` should be at least 50 characters in length and contain a random mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key.
|
||||
`SECRET_KEY` **must** be at least 50 characters in length, and should contain a mix of letters, digits, and symbols. The script located at `$INSTALL_ROOT/netbox/generate_secret_key.py` may be used to generate a suitable key. Please note that this key is **not** used directly for hashing user passwords or for the encrypted storage of secret data in NetBox.
|
||||
|
@ -73,6 +73,14 @@ Determines if localization features are enabled or not. This should only be enab
|
||||
|
||||
---
|
||||
|
||||
## GIT_PATH
|
||||
|
||||
Default: `git`
|
||||
|
||||
The system path to the `git` executable, used by the synchronization backend for remote git repositories.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
|
@ -16,6 +16,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
|
||||
* Decimal: A fixed-precision decimal number (4 decimal places)
|
||||
* Boolean: True or false
|
||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||
* Date & time: A date and time in ISO 8601 format (YYYY-MM-DD HH:MM:SS)
|
||||
* URL: This will be presented as a link in the web UI
|
||||
* JSON: Arbitrary data stored in JSON format
|
||||
* Selection: A selection of one of several pre-defined custom choices
|
||||
@ -35,18 +36,12 @@ The filter logic controls how values are matched when filtering objects by the c
|
||||
|
||||
### Grouping
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.3.
|
||||
|
||||
Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.)
|
||||
|
||||
This parameter has no effect on the API representation of custom field data.
|
||||
|
||||
### Visibility
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.3.
|
||||
|
||||
When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI.
|
||||
|
||||
* **Read/write** (default): The custom field is included when viewing and editing objects.
|
||||
|
@ -27,7 +27,6 @@ The following context data is available within the template when rendering a cus
|
||||
| Variable | Description |
|
||||
|-----------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| `object` | The NetBox object being displayed |
|
||||
| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 |
|
||||
| `debug` | A boolean indicating whether debugging is enabled |
|
||||
| `request` | The current WSGI request |
|
||||
| `user` | The current user (if authenticated) |
|
||||
|
@ -35,12 +35,9 @@ class MyScript(Script):
|
||||
|
||||
The `run()` method should accept two arguments:
|
||||
|
||||
* `data` - A dictionary containing all of the variable data passed via the web form.
|
||||
* `data` - A dictionary containing all the variable data passed via the web form.
|
||||
* `commit` - A boolean indicating whether database changes will be committed.
|
||||
|
||||
!!! note
|
||||
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.)
|
||||
|
||||
Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed.
|
||||
|
||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||
@ -104,12 +101,14 @@ The checkbox to commit database changes when executing a script is checked by de
|
||||
commit_default = False
|
||||
```
|
||||
|
||||
### `scheduling_enabled`
|
||||
|
||||
By default, a script can be scheduled for execution at a later time. Setting `scheduling_enabled` to False disables this ability: Only immediate execution will be possible. (This also disables the ability to set a recurring execution interval.)
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Accessing Request Data
|
||||
|
||||
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:
|
||||
|
@ -91,12 +91,14 @@ As you can see, reports are completely customizable. Validation logic can be as
|
||||
|
||||
A human-friendly description of what your report does.
|
||||
|
||||
### `scheduling_enabled`
|
||||
|
||||
By default, a report can be scheduled for execution at a later time. Setting `scheduling_enabled` to False disables this ability: Only immediate execution will be possible. (This also disables the ability to set a recurring execution interval.)
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Logging
|
||||
|
||||
The following methods are available to log results within a report:
|
||||
|
@ -8,6 +8,14 @@ The registry can be inspected by importing `registry` from `extras.registry`.
|
||||
|
||||
## Stores
|
||||
|
||||
### `data_backends`
|
||||
|
||||
A dictionary mapping data backend types to their respective classes. These are used to interact with [remote data sources](../models/core/datasource.md).
|
||||
|
||||
### `denormalized_fields`
|
||||
|
||||
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
|
||||
|
||||
### `model_features`
|
||||
|
||||
A dictionary of particular features (e.g. custom fields) mapped to the NetBox models which support them, arranged by app. For example:
|
||||
@ -20,38 +28,23 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
|
||||
...
|
||||
},
|
||||
'webhooks': {
|
||||
...
|
||||
'extras': ['configcontext', 'tag', ...],
|
||||
'dcim': ['site', 'rack', 'devicetype', ...],
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### `plugin_menu_items`
|
||||
Supported model features are listed in the [features matrix](./models.md#features-matrix).
|
||||
|
||||
Navigation menu items provided by NetBox plugins. Each plugin is registered as a key with the list of menu items it provides. An example:
|
||||
### `plugins`
|
||||
|
||||
```python
|
||||
{
|
||||
'Plugin A': (
|
||||
<MenuItem>, <MenuItem>, <MenuItem>,
|
||||
),
|
||||
'Plugin B': (
|
||||
<MenuItem>, <MenuItem>, <MenuItem>,
|
||||
),
|
||||
}
|
||||
```
|
||||
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
|
||||
|
||||
### `plugin_template_extensions`
|
||||
### `search`
|
||||
|
||||
Plugin content that gets embedded into core NetBox templates. The store comprises NetBox models registered as dictionary keys, each pointing to a list of applicable template extension classes that exist. An example:
|
||||
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
|
||||
|
||||
```python
|
||||
{
|
||||
'dcim.site': [
|
||||
<TemplateExtension>, <TemplateExtension>, <TemplateExtension>,
|
||||
],
|
||||
'dcim.rack': [
|
||||
<TemplateExtension>, <TemplateExtension>,
|
||||
],
|
||||
}
|
||||
```
|
||||
### `views`
|
||||
|
||||
A hierarchical mapping of registered views for each model. Mappings are added using the `register_model_view()` decorator, and URLs paths can be generated from these using `get_model_urls()`.
|
||||
|
@ -2,38 +2,44 @@
|
||||
|
||||
## Model Types
|
||||
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own table in the PostgreSQL database. All NetBox data models can be categorized by type.
|
||||
|
||||
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
|
||||
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework is used to map Django models to database tables. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model within the DCIM app is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
|
||||
|
||||
### Features Matrix
|
||||
|
||||
* [Change logging](../features/change-logging.md) - Changes to these objects are automatically recorded in the change log
|
||||
* [Webhooks](../integrations/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
|
||||
* [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](../features/journaling.md) - These models support persistent historical commentary
|
||||
* Nesting - These models can be nested recursively to create a hierarchy
|
||||
Depending on its classification, each NetBox model may support various features which enhance its operation. Each feature is enabled by inheriting from its designated mixin class, and some features also make use of the [application registry](./application-registry.md#model_features).
|
||||
|
||||
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
||||
| ------------------ | ---------------- | ---------------- |------------------| ---------------- | ---------------- | ---------------- | ---------------- |
|
||||
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
||||
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
|
||||
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||
| Component Template | :material-check: | :material-check: | | | | | |
|
||||
| Feature | Feature Mixin | Registry Key | Description |
|
||||
|------------------------------------------------------------|-------------------------|--------------------|--------------------------------------------------------------------------------|
|
||||
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log |
|
||||
| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy |
|
||||
| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
|
||||
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
|
||||
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
|
||||
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
|
||||
| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Users can create custom export templates for these models |
|
||||
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
|
||||
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
|
||||
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
|
||||
| [Webhooks](../integrations/webhooks.md) | `WebhooksMixin` | `webhooks` | NetBox is capable of generating outgoing webhooks for these objects |
|
||||
|
||||
## Models Index
|
||||
|
||||
### Primary Models
|
||||
|
||||
These are considered the "core" application models which are used to model network infrastructure.
|
||||
|
||||
* [circuits.Circuit](../models/circuits/circuit.md)
|
||||
* [circuits.Provider](../models/circuits/provider.md)
|
||||
* [circuits.ProviderAccount](../models/circuits/provideracount.md)
|
||||
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
||||
* [core.DataSource](../models/core/datasource.md)
|
||||
* [dcim.Cable](../models/dcim/cable.md)
|
||||
* [dcim.Device](../models/dcim/device.md)
|
||||
* [dcim.DeviceType](../models/dcim/devicetype.md)
|
||||
* [dcim.Module](../models/dcim/module.md)
|
||||
* [dcim.ModuleType](../models/dcim/moduletype.md)
|
||||
* [dcim.PowerFeed](../models/dcim/powerfeed.md)
|
||||
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
|
||||
* [dcim.Rack](../models/dcim/rack.md)
|
||||
@ -47,10 +53,10 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [ipam.IPAddress](../models/ipam/ipaddress.md)
|
||||
* [ipam.IPRange](../models/ipam/iprange.md)
|
||||
* [ipam.L2VPN](../models/ipam/l2vpn.md)
|
||||
* [ipam.L2VPNTermination](../models/ipam/l2vpntermination.md)
|
||||
* [ipam.Prefix](../models/ipam/prefix.md)
|
||||
* [ipam.RouteTarget](../models/ipam/routetarget.md)
|
||||
* [ipam.Service](../models/ipam/service.md)
|
||||
* [ipam.ServiceTemplate](../models/ipam/servicetemplate.md)
|
||||
* [ipam.VLAN](../models/ipam/vlan.md)
|
||||
* [ipam.VRF](../models/ipam/vrf.md)
|
||||
* [tenancy.Contact](../models/tenancy/contact.md)
|
||||
@ -62,6 +68,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
### Organizational Models
|
||||
|
||||
Organization models are used to organize and classify primary models.
|
||||
|
||||
* [circuits.CircuitType](../models/circuits/circuittype.md)
|
||||
* [dcim.DeviceRole](../models/dcim/devicerole.md)
|
||||
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
||||
@ -76,6 +84,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
### Nested Group Models
|
||||
|
||||
Nested group models behave like organizational model, but self-nest within a recursive hierarchy. For example, the Region model can be used to represent a hierarchy of countries, states, and cities.
|
||||
|
||||
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
|
||||
* [dcim.Region](../models/dcim/region.md)
|
||||
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
|
||||
@ -85,12 +95,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
### Component Models
|
||||
|
||||
Component models represent individual physical or virtual components belonging to a device or virtual machine.
|
||||
|
||||
* [dcim.ConsolePort](../models/dcim/consoleport.md)
|
||||
* [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md)
|
||||
* [dcim.DeviceBay](../models/dcim/devicebay.md)
|
||||
* [dcim.FrontPort](../models/dcim/frontport.md)
|
||||
* [dcim.Interface](../models/dcim/interface.md)
|
||||
* [dcim.InventoryItem](../models/dcim/inventoryitem.md)
|
||||
* [dcim.ModuleBay](../models/dcim/modulebay.md)
|
||||
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
|
||||
* [dcim.PowerPort](../models/dcim/powerport.md)
|
||||
* [dcim.RearPort](../models/dcim/rearport.md)
|
||||
@ -98,11 +111,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
### Component Template Models
|
||||
|
||||
These function as templates to effect the replication of device and virtual machine components. Component template models support a limited feature set, including change logging, custom validation, and webhooks.
|
||||
|
||||
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
|
||||
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
|
||||
* [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md)
|
||||
* [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md)
|
||||
* [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md)
|
||||
* [dcim.InventoryItemTemplate](../models/dcim/inventoryitemtemplate.md)
|
||||
* [dcim.ModuleBayTemplate](../models/dcim/modulebaytemplate.md)
|
||||
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
|
||||
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
|
||||
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)
|
||||
|
@ -29,7 +29,7 @@ A SearchIndex subclass defines both its model and a list of two-tuples specifyin
|
||||
| 60 | Unique serialized attribute (per related object) | Device.serial |
|
||||
| 100 | Primary human identifier | Device.name, Circuit.cid, Cable.label |
|
||||
| 110 | Slug | Site.slug |
|
||||
| 200 | Secondary identifier | Provider.account, DeviceType.part_number |
|
||||
| 200 | Secondary identifier | ProviderAccount.account, DeviceType.part_number |
|
||||
| 300 | Highly unique descriptive attribute | CircuitTermination.xconnect_id, IPAddress.dns_name |
|
||||
| 500 | Description | Site.description |
|
||||
| 1000 | Custom field default | - |
|
||||
|
@ -30,12 +30,6 @@ A webhook is a mechanism for conveying to some external system a change that too
|
||||
|
||||
To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
|
||||
|
||||
## NAPALM
|
||||
|
||||
[NAPALM](https://github.com/napalm-automation/napalm) is a Python library which enables direct interaction with network devices of various platforms. When configured, NetBox supports fetching live operational and status data directly from network devices to be compared to what has been defined in NetBox. This allows for easily validating the device's operational state against its desired state. Additionally, NetBox's REST API can act as a sort of proxy for NAPALM commands, allowing external clients to interact with network devices by sending HTTP requests to the appropriate API endpoint.
|
||||
|
||||
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).
|
||||
|
13
docs/features/background-jobs.md
Normal file
13
docs/features/background-jobs.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Background Jobs
|
||||
|
||||
NetBox includes the ability to execute certain functions as background tasks. These include:
|
||||
|
||||
* [Report](../customization/reports.md) execution
|
||||
* [Custom script](../customization/custom-scripts.md) execution
|
||||
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
|
||||
|
||||
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).
|
||||
|
||||
## Scheduled Jobs
|
||||
|
||||
Background jobs can be configured to run immediately, or at a set time in the future. Scheduled jobs can also be configured to repeat at a set interval.
|
@ -1,9 +1,13 @@
|
||||
# Change Logging
|
||||
|
||||
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
|
||||
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with metadata including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
|
||||
|
||||
A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.
|
||||
|
||||
When a request is made, a UUID is generated and attached to any change records resulting from that request. For example, editing three objects in bulk will create a separate change record for each (three in total), and each of those objects will be associated with the same UUID. This makes it easy to identify all the change records resulting from a particular request.
|
||||
|
||||
Change records are exposed in the API via the read-only endpoint `/api/extras/object-changes/`. They may also be exported via the web UI in CSV format.
|
||||
|
||||
## Correlating Changes by Request
|
||||
|
||||
Every request made to NetBox is assigned a random unique ID that can be used to correlate change records. For example, if you change the status of three sites using the UI's bulk edit feature, you will see three new change records (one for each site) all referencing the same request ID. This shows that all three changes were made as part of the same request.
|
||||
|
@ -5,13 +5,15 @@ NetBox is ideal for managing your network's transit and peering providers and ci
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ASN --> Provider
|
||||
Provider --> ProviderNetwork & Circuit
|
||||
Provider --> ProviderNetwork & ProviderAccount & Circuit
|
||||
ProviderAccount --> Circuit
|
||||
CircuitType --> Circuit
|
||||
|
||||
click ASN "../../models/circuits/asn/"
|
||||
click Circuit "../../models/circuits/circuit/"
|
||||
click CircuitType "../../models/circuits/circuittype/"
|
||||
click Provider "../../models/circuits/provider/"
|
||||
click ProviderAccount "../../models/circuits/provideraccount/"
|
||||
click ProviderNetwork "../../models/circuits/providernetwork/"
|
||||
```
|
||||
|
||||
@ -25,7 +27,7 @@ Sometimes you'll need to model provider networks into which you don't have full
|
||||
|
||||
A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox.
|
||||
|
||||
Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics.
|
||||
Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. Provider accounts can also be employed to further categorize circuits belonging to a common provider: These may represent different business units or technologies.
|
||||
|
||||
Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity.
|
||||
|
||||
|
81
docs/features/configuration-rendering.md
Normal file
81
docs/features/configuration-rendering.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Configuration Rendering
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ConfigContext & ConfigTemplate --> Config{{Rendered configuration}}
|
||||
|
||||
click ConfigContext "../../models/extras/configcontext/"
|
||||
click ConfigTemplate "../../models/extras/configtemplate/"
|
||||
```
|
||||
|
||||
## Configuration Templates
|
||||
|
||||
Configuration templates are written in the [Jinja2 templating language](https://jinja.palletsprojects.com/), and may be automatically populated from remote data sources. Context data is applied to a template during rendering to output a complete configuration file. Below is an example Jinja2 template which renders a simple network switch configuration file.
|
||||
|
||||
```jinja2
|
||||
{% extends 'base.j2' %}
|
||||
|
||||
{% block content %}
|
||||
system {
|
||||
host-name {{ device.name }};
|
||||
domain-name example.com;
|
||||
time-zone UTC;
|
||||
authentication-order [ password radius ];
|
||||
ntp {
|
||||
{% for server in ntp_servers %}
|
||||
server {{ server }};
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% for interface in device.interfaces.all() %}
|
||||
{% include 'common/interface.j2' %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
When rendered for a specific NetBox device, the template's `device` variable will be populated with the device instance, and `ntp_servers` will be pulled from the device's available context data. The resulting output will be a valid configuration segment that can be applied directly to a compatible network device.
|
||||
|
||||
## Rendering Templates
|
||||
|
||||
### Device Configurations
|
||||
|
||||
NetBox provides a REST API endpoint specifically for rendering the default configuration template for a specific device. This is accomplished by sending a POST request to the device's unique URL, optionally including additional context data.
|
||||
|
||||
```no-highlight
|
||||
curl -X POST \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
http://netbox:8000/api/dcim/devices/123/render-config/ \
|
||||
--data '{
|
||||
"extra_data": "abc123"
|
||||
}'
|
||||
```
|
||||
|
||||
This request will trigger resolution of the device's preferred config template in the following order:
|
||||
|
||||
* The config template assigned to the individual device
|
||||
* The config template assigned to the device's role
|
||||
* The config template assigned to the device's platform
|
||||
|
||||
If no config template has been assigned to any of these three objects, the request will fail.
|
||||
|
||||
### General Purpose Use
|
||||
|
||||
NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template.
|
||||
|
||||
```no-highlight
|
||||
curl -X POST \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
http://netbox:8000/api/extras/config-templates/123/render/ \
|
||||
--data '{
|
||||
"foo": "abc",
|
||||
"bar": 123
|
||||
}'
|
||||
```
|
@ -31,6 +31,7 @@ The following models support the assignment of contacts:
|
||||
|
||||
* circuits.Circuit
|
||||
* circuits.Provider
|
||||
* circuits.ProviderAccount
|
||||
* dcim.Device
|
||||
* dcim.Location
|
||||
* dcim.Manufacturer
|
||||
|
@ -11,6 +11,8 @@ Configuration context data (or "config contexts" for short) is a powerful featur
|
||||
}
|
||||
```
|
||||
|
||||
Context data can be consumed by remote API clients, or it can be employed natively to render [configuration templates](./configuration-rendering.md).
|
||||
|
||||
Config contexts can be computed for objects based on the following criteria:
|
||||
|
||||
| Type | Devices | Virtual Machines |
|
||||
|
23
docs/features/synchronized-data.md
Normal file
23
docs/features/synchronized-data.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Synchronized Data
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models.
|
||||
|
||||
To enable remote data synchronization, the NetBox administrator first designates one or more remote data sources. NetBox currently supports the following source types:
|
||||
|
||||
* Git repository
|
||||
* Amazon S3 bucket (or compatible product)
|
||||
* Local disk path
|
||||
|
||||
(Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.)
|
||||
|
||||
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
|
||||
|
||||
The following NetBox models can be associated with replicated data files:
|
||||
|
||||
* Config contexts
|
||||
* Config templates
|
||||
* Export templates
|
||||
|
||||
Once a data has been designated for a local instance, its data will be replaced with the content of the replicated file. When the replicated file is updated in the future (via synchronization jobs), the local instance will be flagged as having out-of-date data. A user can then synchronize these objects individually or in bulk to effect the update. This two-stage process ensures that automated synchronization tasks do not immediately affect production data.
|
@ -56,7 +56,7 @@ Below is the (rough) recommended order in which NetBox objects should be created
|
||||
4. Manufacturers, device types, and module types
|
||||
5. Platforms and device roles
|
||||
6. Devices and modules
|
||||
7. Providers and provider networks
|
||||
7. Providers, provider accounts, and provider networks
|
||||
8. Circuit types and circuits
|
||||
9. Wireless LAN groups and wireless LANs
|
||||
10. Route targets and VRFs
|
||||
|
@ -28,12 +28,12 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
Once PostgreSQL has been installed, start the service and enable it to run at boot:
|
||||
Once PostgreSQL has been installed, start the service and enable it to run at boot:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
```
|
||||
```no-highlight
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 11 or later:
|
||||
|
||||
|
@ -4,9 +4,6 @@
|
||||
|
||||
[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md).
|
||||
|
||||
!!! warning "Redis v4.0 or later required"
|
||||
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
```no-highlight
|
||||
|
@ -199,14 +199,6 @@ When you have finished modifying the configuration, remember to save the file.
|
||||
|
||||
All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory.
|
||||
|
||||
### NAPALM
|
||||
|
||||
Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
### Remote File Storage
|
||||
|
||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/_y5JRiW_PLM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
|
@ -97,7 +97,7 @@ sudo git pull origin master
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
|
||||
```no-highlight
|
||||
sudo ./upgrade.sh
|
||||
|
@ -1,74 +0,0 @@
|
||||
# NAPALM
|
||||
|
||||
NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
|
||||
|
||||
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
|
||||
|
||||
* Device status is "Active"
|
||||
* A primary IP has been assigned to the device
|
||||
* A platform with a NAPALM driver has been assigned
|
||||
* The authenticated user has the `dcim.napalm_read_device` permission
|
||||
|
||||
!!! note
|
||||
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.
|
||||
|
||||
Below is an example REST API request and response:
|
||||
|
||||
```no-highlight
|
||||
GET /api/dcim/devices/1/napalm/?method=get_environment
|
||||
|
||||
{
|
||||
"get_environment": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
To make NAPALM requests via the NetBox REST API, a NetBox user must have assigned a permission granting the `napalm_read` action for the device object type.
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-NAPALM-Username: foo" \
|
||||
-H "X-NAPALM-Password: bar"
|
||||
```
|
||||
|
||||
## Method Support
|
||||
|
||||
The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. Because there is no granular mechanism in place for limiting potentially disruptive requests, NetBox supports only read-only [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
|
||||
|
||||
## Multiple Methods
|
||||
|
||||
It is possible to request the output of multiple NAPALM methods in a single API request by passing multiple `method` parameters. For example:
|
||||
|
||||
```no-highlight
|
||||
GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers
|
||||
|
||||
{
|
||||
"get_ntp_servers": {
|
||||
...
|
||||
},
|
||||
"get_ntp_peers": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Arguments
|
||||
|
||||
The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`. For example, the SSH port is changed to 2222 in this API call:
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-NAPALM-port: 2222"
|
||||
```
|
@ -662,3 +662,28 @@ Note that we are _not_ passing an existing REST API token with this request. If
|
||||
"description": ""
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP Headers
|
||||
|
||||
### `API-Version`
|
||||
|
||||
This header specifies the API version in use. This will always match the version of NetBox installed. For example, NetBox v3.4.2 will report an API version of `3.4`.
|
||||
|
||||
### `X-Request-ID`
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
This header specifies the unique ID assigned to the received API request. It can be very handy for correlating a request with change records. For example, after creating several new objects, you can filter against the object changes API endpoint to retrieve the resulting change records:
|
||||
|
||||
```
|
||||
GET /api/extras/object-changes/?request_id=e39c84bc-f169-4d5f-bc1c-94487a1b18b5
|
||||
```
|
||||
|
||||
The request ID can also be used to filter many objects directly, to return those created or updated by a certain request:
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?created_by_request=e39c84bc-f169-4d5f-bc1c-94487a1b18b5
|
||||
```
|
||||
|
||||
!!! note
|
||||
This header is included with _all_ NetBox responses, although it is most practical when working with an API.
|
||||
|
9
docs/integrations/synchronized-data.md
Normal file
9
docs/integrations/synchronized-data.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Synchronized Data
|
||||
|
||||
Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md).
|
||||
|
||||
The following features support the use of synchronized data:
|
||||
|
||||
* [Configuration templates](../features/configuration-rendering.md)
|
||||
* [Configuration context data](../features/context-data.md)
|
||||
* [Export templates](../customization/export-templates.md)
|
@ -37,7 +37,6 @@ NetBox was built specifically to serve the needs of network engineers and operat
|
||||
* Robust object-based permissions
|
||||
* Detailed, automatic change logging
|
||||
* Global search engine
|
||||
* NAPALM integration
|
||||
|
||||
## What NetBox Is Not
|
||||
|
||||
@ -78,4 +77,3 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 11+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
| Live device access | NAPALM (optional) |
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 171 KiB |
@ -8,6 +8,10 @@ A circuit represents a physical point-to-point data connection, typically used t
|
||||
|
||||
The [provider](./provider.md) to which this circuit belongs.
|
||||
|
||||
### Provider Account
|
||||
|
||||
Circuits may optionally be assigned to a specific [provider account](./provideraccount.md).
|
||||
|
||||
### Circuit ID
|
||||
|
||||
An identifier for this circuit. This must be unique to the assigned provider. (Circuits assigned to different providers may have the same circuit ID.)
|
||||
|
@ -12,21 +12,10 @@ A unique human-friendly name.
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
||||
### ASN
|
||||
|
||||
The AS number assigned to this provider.
|
||||
|
||||
!!! warning "Legacy field"
|
||||
This field is being removed in NetBox v3.4. Users are highly encouraged to use the [ASN model](../ipam/asn.md) to track AS number assignment for providers.
|
||||
|
||||
### ASNs
|
||||
|
||||
The [AS numbers](../ipam/asn.md) assigned to this provider (optional).
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
||||
|
||||
### Portal URL
|
||||
|
||||
The URL for the provider's customer service portal.
|
||||
|
19
docs/models/circuits/provideraccount.md
Normal file
19
docs/models/circuits/provideraccount.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Provider Accounts
|
||||
|
||||
!!! info "This model was introduced in NetBox v3.5."
|
||||
|
||||
This model can be used to represent individual accounts associated with a provider.
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider
|
||||
|
||||
The [provider](./provider.md) the account belongs to.
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name, unique to the provider.
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
25
docs/models/core/datafile.md
Normal file
25
docs/models/core/datafile.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Data Files
|
||||
|
||||
A data file object is the representation in NetBox's database of some file belonging to a remote [data source](./datasource.md). Data files are synchronized automatically, and cannot be modified locally (although they can be deleted).
|
||||
|
||||
## Fields
|
||||
|
||||
### Source
|
||||
|
||||
The [data source](./datasource.md) to which this file belongs.
|
||||
|
||||
### Path
|
||||
|
||||
The path to the file, relative to its source's URL. For example, a file at `/opt/config-data/routing/bgp/peer.yaml` with a source URL of `file:///opt/config-data/` would have its path set to `routing/bgp/peer.yaml`.
|
||||
|
||||
### Last Updated
|
||||
|
||||
The date and time at which the file most recently updated from its source. Note that this attribute is updated only when the file's contents have been modified. Re-synchronizing the data source will not update this timestamp if the upstream file's data has not changed.
|
||||
|
||||
### Size
|
||||
|
||||
The file's size, in bytes.
|
||||
|
||||
### Hash
|
||||
|
||||
A [SHA256 hash](https://en.wikipedia.org/wiki/SHA-2) of the file's data. This can be compared to a hash taken from the original file to determine whether any changes have been made.
|
49
docs/models/core/datasource.md
Normal file
49
docs/models/core/datasource.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Data Sources
|
||||
|
||||
A data source represents some external repository of data which NetBox can consume, such as a git repository. Files within the data source are synchronized to NetBox by saving them in the database as [data file](./datafile.md) objects.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The data source's human-friendly name.
|
||||
|
||||
### Type
|
||||
|
||||
The type of data source. Supported options include:
|
||||
|
||||
* Local directory
|
||||
* git repository
|
||||
* Amazon S3 bucket
|
||||
|
||||
### URL
|
||||
|
||||
The URL identifying the remote source. Some examples are included below.
|
||||
|
||||
| Type | Example URL |
|
||||
|-----------|----------------------------------------------------|
|
||||
| Local | file:///path/to/my/data/ |
|
||||
| git | https://github.com/my-organization/my-repo |
|
||||
| Amazon S3 | https://s3.us-east-2.amazonaws.com/my-bucket-name/ |
|
||||
|
||||
### Status
|
||||
|
||||
The source's current synchronization status. Note that this cannot be set manually: It is updated automatically when the source is synchronized.
|
||||
|
||||
### Enabled
|
||||
|
||||
If false, synchronization will be disabled.
|
||||
|
||||
### Ignore Rules
|
||||
|
||||
A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
|
||||
|
||||
| Rule | Description |
|
||||
|----------------|------------------------------------------|
|
||||
| `README` | Ignore any files named `README` |
|
||||
| `*.txt` | Ignore any files with a `.txt` extension |
|
||||
| `data???.json` | Ignore e.g. `data123.json` |
|
||||
|
||||
### Last Synced
|
||||
|
||||
The date and time at which the source was most recently synchronized successfully.
|
54
docs/models/core/job.md
Normal file
54
docs/models/core/job.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Jobs
|
||||
|
||||
The Job model is used to schedule and record the execution of [background tasks](../../features/background-jobs.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The name or other identifier of the NetBox object with which the job is associated.
|
||||
|
||||
## Object Type
|
||||
|
||||
The type of object (model) associated with this job.
|
||||
|
||||
### Created
|
||||
|
||||
The date and time at which the job itself was created.
|
||||
|
||||
### Scheduled
|
||||
|
||||
The date and time at which the job is/was scheduled to execute (if not submitted for immediate execution at the time of creation).
|
||||
|
||||
### Interval
|
||||
|
||||
The interval (in minutes) at which a scheduled job should re-execute.
|
||||
|
||||
### Completed
|
||||
|
||||
The date and time at which the job completed (if complete).
|
||||
|
||||
### User
|
||||
|
||||
The user who created the job.
|
||||
|
||||
### Status
|
||||
|
||||
The job's current status. Potential values include:
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| Pending | Awaiting execution by an RQ worker process |
|
||||
| Scheduled | Scheduled for a future date/time |
|
||||
| Running | Currently executing |
|
||||
| Completed | Successfully completed |
|
||||
| Failed | The job did not complete successfully |
|
||||
| Errored | An unexpected error was encountered during execution |
|
||||
|
||||
### Data
|
||||
|
||||
Any data associated with the execution of the job, such as log output.
|
||||
|
||||
### Job ID
|
||||
|
||||
The job's UUID, used for unique identification within a queue.
|
@ -72,6 +72,10 @@ The device's operational status.
|
||||
|
||||
A device may be associated with a particular [platform](./platform.md) to indicate its operating system. Note that only platforms assigned to the associated manufacturer (or to no manufacturer) will be available for selection.
|
||||
|
||||
### Configuration Template
|
||||
|
||||
The [configuration template](../extras/configtemplate.md) from which the configuration for this device can be rendered. If set, this will override any config template referenced by the device's role or platform.
|
||||
|
||||
### Primary IPv4 & IPv6 Addresses
|
||||
|
||||
Each device may designate one primary IPv4 address and/or one primary IPv6 address for management purposes.
|
||||
|
@ -19,3 +19,7 @@ The color used when displaying the role in the NetBox UI.
|
||||
### VM Role
|
||||
|
||||
If selected, this role may be assigned to [virtual machines](../virtualization/virtualmachine.md)
|
||||
|
||||
### Configuration Template
|
||||
|
||||
The default [configuration template](../extras/configtemplate.md) for devices assigned to this role.
|
||||
|
@ -21,6 +21,10 @@ The model number assigned to this device type by its manufacturer. Must be uniqu
|
||||
|
||||
A unique URL-friendly representation of the model identifier. (This value can be used for filtering.)
|
||||
|
||||
### Default Platform
|
||||
|
||||
If defined, devices instantiated from this type will automatically inherit the selected platform. (This assignment can be changed after the device has been created.)
|
||||
|
||||
### Part Number
|
||||
|
||||
An alternative part number to uniquely identify the device type.
|
||||
|
@ -22,10 +22,20 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
||||
If designated, this platform will be available for use only to devices assigned to this [manufacturer](./manufacturer.md). This can be handy e.g. for limiting network operating systems to use on hardware produced by the relevant vendor. However, it should not be used when defining general-purpose software platforms.
|
||||
|
||||
### Configuration Template
|
||||
|
||||
The default [configuration template](../extras/configtemplate.md) for devices assigned to this platform.
|
||||
|
||||
### NAPALM Driver
|
||||
|
||||
!!! warning "Deprecated Field"
|
||||
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
|
||||
|
||||
The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.
|
||||
|
||||
### NAPALM Arguments
|
||||
|
||||
!!! warning "Deprecated Field"
|
||||
NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
|
||||
|
||||
Any additional arguments to send when invoking the NAPALM driver assigned to this platform.
|
||||
|
@ -18,6 +18,10 @@ A numeric value which influences the order in which context data is merged. Cont
|
||||
|
||||
The context data expressed in JSON format.
|
||||
|
||||
### Data File
|
||||
|
||||
Config context data may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local data for the config context: It will be populated automatically from the data file.
|
||||
|
||||
### Is Active
|
||||
|
||||
If not selected, this config context will be excluded from rendering. This can be convenient to temporarily disable a config context.
|
||||
|
29
docs/models/extras/configtemplate.md
Normal file
29
docs/models/extras/configtemplate.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Configuration Templates
|
||||
|
||||
Configuration templates can be used to render [device](../dcim/device.md) configurations from [context data](../../features/context-data.md). Templates are written in the [Jinja2 language](https://jinja.palletsprojects.com/) and can be associated with devices roles, platforms, and/or individual devices.
|
||||
|
||||
Context data is made available to [devices](../dcim/device.md) and/or [virtual machines](../virtualization/virtualmachine.md) based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster.
|
||||
|
||||
See the [configuration rendering documentation](../../features/configuration-rendering.md) for more information.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Weight
|
||||
|
||||
A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.
|
||||
|
||||
### Data File
|
||||
|
||||
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
|
||||
|
||||
### Template Code
|
||||
|
||||
Jinja2 template code, if being defined locally rather than replicated from a data file.
|
||||
|
||||
### Environment Parameters
|
||||
|
||||
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
|
@ -82,6 +82,10 @@ The default value to populate for the custom field when creating new objects (op
|
||||
|
||||
For choice and multi-choice custom fields only. A comma-delimited list of the available choices.
|
||||
|
||||
### Cloneable
|
||||
|
||||
If enabled, values from this field will be automatically pre-populated when cloning existing objects.
|
||||
|
||||
### Minimum Value
|
||||
|
||||
For numeric custom fields only. The minimum valid value (optional).
|
||||
|
@ -12,6 +12,10 @@ The name of the export template. This will appear in the "export" dropdown list
|
||||
|
||||
The type of NetBox object to which the export template applies.
|
||||
|
||||
### Data File
|
||||
|
||||
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local content for the template: It will be populated automatically from the data file.
|
||||
|
||||
### Template Code
|
||||
|
||||
Jinja2 template code for rendering the exported data.
|
||||
|
@ -22,11 +22,13 @@ If not selected, the webhook will be inactive.
|
||||
|
||||
The events which will trigger the webhook. At least one event type must be selected.
|
||||
|
||||
| Name | Description |
|
||||
|-----------|--------------------------------------|
|
||||
| Creations | A new object has been created |
|
||||
| Updates | An existing object has been modified |
|
||||
| Deletions | An object has been deleted |
|
||||
| Name | Description |
|
||||
|------------|--------------------------------------|
|
||||
| Creations | A new object has been created |
|
||||
| Updates | An existing object has been modified |
|
||||
| Deletions | An object has been deleted |
|
||||
| Job starts | A job for an object starts |
|
||||
| Job ends | A job for an object terminates |
|
||||
|
||||
### URL
|
||||
|
||||
@ -58,6 +60,10 @@ Jinja2 template for a custom request body, if desired. If not defined, NetBox wi
|
||||
|
||||
A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
|
||||
|
||||
### Conditions
|
||||
|
||||
A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, the webhook will not be sent. A webhook that does not define any conditions will _always_ trigger.
|
||||
|
||||
### SSL Verification
|
||||
|
||||
Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used.
|
||||
|
@ -1,8 +1,8 @@
|
||||
# ASN
|
||||
# ASNs
|
||||
|
||||
An Autonomous System Number (ASN) is a numeric identifier used in the BGP protocol to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating and transiting through. NetBox support both 32- and 64- ASNs.
|
||||
|
||||
ASNs must be globally unique within NetBox, must each may be assigned to multiple [sites](../dcim/site.md).
|
||||
ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md).
|
||||
|
||||
## Fields
|
||||
|
||||
|
23
docs/models/ipam/asnrange.md
Normal file
23
docs/models/ipam/asnrange.md
Normal file
@ -0,0 +1,23 @@
|
||||
# ASN Ranges
|
||||
|
||||
!!! info "This model was introduced in NetBox v3.5."
|
||||
|
||||
Ranges can be defined to group [AS numbers](./asn.md) numerically and to facilitate their automatic provisioning. Each range must be assigned to a [RIR](./rir.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name for the range.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
|
||||
### RIR
|
||||
|
||||
The [Regional Internet Registry](./rir.md) or similar authority responsible for the allocation of AS numbers within this range.
|
||||
|
||||
### Start & End
|
||||
|
||||
The starting and ending numeric boundaries of the range (inclusive).
|
@ -28,3 +28,7 @@ The IP range's operational status. Note that the status of a range does _not_ ha
|
||||
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Mark Utilized
|
||||
|
||||
If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example.
|
||||
|
@ -6,11 +6,11 @@ A tenant represents a discrete grouping of resources used for administrative pur
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
A human-friendly name, unique to the assigned group.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
A URL-friendly identifier, unique to the assigned group. (This value can be used for filtering.)
|
||||
|
||||
### Group
|
||||
|
||||
|
51
docs/plugins/development/dashboard-widgets.md
Normal file
51
docs/plugins/development/dashboard-widgets.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Dashboard Widgets
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.5."
|
||||
|
||||
Each NetBox user can customize his or her personal dashboard by adding and removing widgets and by manipulating the size and position of each. Plugins can register their own dashboard widgets to complement those already available natively.
|
||||
|
||||
## The DashboardWidget Class
|
||||
|
||||
All dashboard widgets must inherit from NetBox's `DashboardWidget` base class. Subclasses must provide a `render()` method, and may override the base class' default characteristics.
|
||||
|
||||
Widgets which require configuration by a user must also include a `ConfigForm` child class which inherits from `WidgetConfigForm`. This form is used to render the user configuration options for the widget.
|
||||
|
||||
::: extras.dashboard.widgets.DashboardWidget
|
||||
|
||||
## Widget Registration
|
||||
|
||||
To register a dashboard widget for use in NetBox, import the `register_widget()` decorator and use it to wrap each `DashboardWidget` subclass:
|
||||
|
||||
```python
|
||||
from extras.dashboard.widgets import DashboardWidget, register_widget
|
||||
|
||||
@register_widget
|
||||
class MyWidget1(DashboardWidget):
|
||||
...
|
||||
|
||||
@register_widget
|
||||
class MyWidget2(DashboardWidget):
|
||||
...
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from extras.dashboard.utils import register_widget
|
||||
from extras.dashboard.widgets import DashboardWidget, WidgetConfigForm
|
||||
|
||||
|
||||
@register_widget
|
||||
class ReminderWidget(DashboardWidget):
|
||||
default_title = 'Reminder'
|
||||
description = 'Add a virtual sticky note'
|
||||
|
||||
class ConfigForm(WidgetConfigForm):
|
||||
content = forms.CharField(
|
||||
widget=forms.Textarea()
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
return self.config.get('content')
|
||||
```
|
@ -70,9 +70,6 @@ class MyModelImportForm(NetBoxModelImportForm):
|
||||
fields = ('name', 'status', 'site', 'comments')
|
||||
```
|
||||
|
||||
!!! note "Previously NetBoxModelCSVForm"
|
||||
This form class was previously named `NetBoxModelCSVForm`. It was renamed in NetBox v3.4 to convey support for JSON and YAML formats in addition to CSV. The `NetBoxModelCSVForm` class has been retained for backward compatibility and functions exactly the same as `NetBoxModelImportForm`. However, plugin authors should be aware that this backward compatability will be removed in NetBox v3.5.
|
||||
|
||||
### `NetBoxModelBulkEditForm`
|
||||
|
||||
This form facilitates editing multiple objects in bulk. Unlike a model form, this form does not have a child `Meta` class, and must explicitly define each field. All fields in a bulk edit form are generally declared with `required=False`.
|
||||
@ -148,74 +145,77 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
|
||||
|
||||
::: utilities.forms.ColorField
|
||||
::: utilities.forms.fields.ColorField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CommentField
|
||||
::: utilities.forms.fields.CommentField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.JSONField
|
||||
::: utilities.forms.fields.JSONField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MACAddressField
|
||||
::: utilities.forms.fields.MACAddressField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.SlugField
|
||||
::: utilities.forms.fields.SlugField
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Choice Fields
|
||||
|
||||
::: utilities.forms.ChoiceField
|
||||
!!! warning "Obsolete Fields"
|
||||
NetBox's custom `ChoiceField` and `MultipleChoiceField` classes are no longer necessary thanks to improvements made to the user interface. Django's native form fields can be used instead. These custom field classes will be removed in NetBox v3.6.
|
||||
|
||||
::: utilities.forms.fields.ChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MultipleChoiceField
|
||||
::: utilities.forms.fields.MultipleChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Dynamic Object Fields
|
||||
|
||||
::: utilities.forms.DynamicModelChoiceField
|
||||
::: utilities.forms.fields.DynamicModelChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.DynamicModelMultipleChoiceField
|
||||
::: utilities.forms.fields.DynamicModelMultipleChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Content Type Fields
|
||||
|
||||
::: utilities.forms.ContentTypeChoiceField
|
||||
::: utilities.forms.fields.ContentTypeChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.ContentTypeMultipleChoiceField
|
||||
::: utilities.forms.fields.ContentTypeMultipleChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
## CSV Import Fields
|
||||
|
||||
::: utilities.forms.CSVChoiceField
|
||||
::: utilities.forms.fields.CSVChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleChoiceField
|
||||
::: utilities.forms.fields.CSVMultipleChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVModelChoiceField
|
||||
::: utilities.forms.fields.CSVModelChoiceField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVContentTypeField
|
||||
::: utilities.forms.fields.CSVContentTypeField
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleContentTypeField
|
||||
::: utilities.forms.fields.CSVMultipleContentTypeField
|
||||
options:
|
||||
members: false
|
||||
|
@ -2,9 +2,6 @@
|
||||
|
||||
## Menus
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Search
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below).
|
||||
|
||||
```python
|
||||
|
@ -3,9 +3,6 @@
|
||||
!!! danger "Experimental Feature"
|
||||
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
|
||||
|
||||
To begin staging changes, first create a [branch](../../models/extras/branch.md):
|
||||
|
@ -74,7 +74,6 @@ This template is used by the `ObjectView` generic view to display a single objec
|
||||
| `breadcrumbs` | - | Breadcrumb list items (HTML `<li>` elements) |
|
||||
| `object_identifier` | - | A unique identifier (string) for the object |
|
||||
| `extra_controls` | - | Additional action buttons to display |
|
||||
| `extra_tabs` | - | Additional tabs to include |
|
||||
|
||||
#### Context
|
||||
|
||||
|
@ -157,9 +157,6 @@ These views are provided to enable or enhance certain NetBox model features, suc
|
||||
|
||||
### Additional Tabs
|
||||
|
||||
!!! note
|
||||
This feature was introduced in NetBox v3.4.
|
||||
|
||||
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
|
||||
|
||||
```python
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
|
||||
|
||||
Plugins are supported on NetBox v2.8 and later.
|
||||
|
||||
## Capabilities
|
||||
|
||||
The NetBox plugin architecture allows for the following:
|
||||
|
132
docs/release-notes/version-3.5.md
Normal file
132
docs/release-notes/version-3.5.md
Normal file
@ -0,0 +1,132 @@
|
||||
# NetBox v3.5
|
||||
|
||||
## v3.5-beta2 (2023-04-18)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The `account` field has been removed from the provider model. This information is now tracked using the new provider account model. Multiple accounts can be assigned per provider.
|
||||
* A minimum length of 50 characters is now enforced for the `SECRET_KEY` configuration parameter.
|
||||
* The JobResult model has been moved from the `extras` app to `core` and renamed to Job. Accordingly, its REST API endpoint has been moved from `/api/extras/job-results/` to `/api/core/jobs/`.
|
||||
* The `obj_type` field on the Job model (previously JobResult) has been renamed to `object_type` for consistency with other models.
|
||||
* The `JOBRESULT_RETENTION` configuration parameter has been renamed to `JOB_RETENTION`.
|
||||
* The `obj` context variable is no longer passed when rendering custom links: Use `object` instead.
|
||||
* The REST API schema is now generated using the OpenAPI 3.0 spec
|
||||
* The URLs for the REST API schema documentation have changed:
|
||||
* `/api/docs/` is now `/api/schema/swagger-ui/`
|
||||
* `/api/redoc/` is now `/api/schema/redoc/`
|
||||
|
||||
### New Features
|
||||
|
||||
#### Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
|
||||
|
||||
The static home view has been replaced with a fully customizable dashboard. Users can construct and rearrange their own personal dashboard to convey the information most pertinent to them. Supported widgets include object statistics, configurable object lists, RSS feeds, and notes, and we expect to continue adding new widgets over time.
|
||||
|
||||
#### Remote Data Sources ([#11558](https://github.com/netbox-community/netbox/issues/11558))
|
||||
|
||||
NetBox now has the ability to synchronize arbitrary data from external sources through the new [DataSource](../models/core/datasource.md) and [DataFile](../models/core/datafile.md) models. Synchronized files are stored in the PostgreSQL database, and may be referenced and consumed by other NetBox models, such as export templates and config contexts. Currently, replication from local filesystem paths, git repositories, and Amazon S3 buckets is supported, and we expect to introduce additional backends in the near future.
|
||||
|
||||
#### Configuration Template Rendering ([#11559](https://github.com/netbox-community/netbox/issues/11559))
|
||||
|
||||
This release introduces the ability to render device configurations from Jinja2 templates natively within NetBox, via both the UI and REST API. The new [ConfigTemplate](../models/extras/configtemplate.md) model stores template code (which may be defined locally or sourced from remote data files). The rendering engine passes data gleaned from both config contexts and request parameters to generate complete configurations suitable for direct application to network devices.
|
||||
|
||||
#### NAPALM Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
|
||||
|
||||
The NAPALM integration feature found in previous NetBox releases has been moved from the core application to a [dedicated plugin](https://github.com/netbox-community/netbox-napalm). This allows greater control over the feature's configuration and will unlock additional potential as a separate project.
|
||||
|
||||
#### ASN Ranges ([#8550](https://github.com/netbox-community/netbox/issues/8550))
|
||||
|
||||
A new ASN range model has been introduced to facilitate the provisioning of new autonomous system numbers from within a prescribed range. For example, an administrator might define an ASN range of 65000-65099 to be used for internal site identification. This includes a REST API endpoint suitable for automatic provisioning, very similar to the allocation of available prefixes and IP addresses.
|
||||
|
||||
#### Provider Accounts ([#9047](https://github.com/netbox-community/netbox/issues/9047))
|
||||
|
||||
A new model has been introduced to represent individual accounts within a common circuit provider. This replaces the `account` field on the provider model, enabling users to track multiple accounts per provider. New provider account instances will be created automatically during upgrade for all providers which currently have an account assigned. The assignment of individual circuits to a provider account remains optional.
|
||||
|
||||
#### Job-Triggered Webhooks ([#8958](https://github.com/netbox-community/netbox/issues/8958))
|
||||
|
||||
Two new webhook trigger events have been introduced: `job_start` and `job_end`. These enable users to configure webhook to trigger when a background job starts or ends, respectively. This new functionality can be used, for example, to inform a remote system when a custom script has been executed.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7947](https://github.com/netbox-community/netbox/issues/7947) - Enable marking IP ranges as fully utilized
|
||||
* [#8184](https://github.com/netbox-community/netbox/issues/8184) - Employ HTMX to dynamically render tables listing related objects
|
||||
* [#8272](https://github.com/netbox-community/netbox/issues/8272) - Support bridge relationships among device type interfaces
|
||||
* [#8749](https://github.com/netbox-community/netbox/issues/8749) - Support replicating custom field values when cloning an object
|
||||
* [#9073](https://github.com/netbox-community/netbox/issues/9073) - Enable syncing config context data from remote sources
|
||||
* [#9653](https://github.com/netbox-community/netbox/issues/9653) - Enable setting a default platform for device types
|
||||
* [#10054](https://github.com/netbox-community/netbox/issues/10054) - Introduce advanced object selector for UI forms
|
||||
* [#10242](https://github.com/netbox-community/netbox/issues/10242) - Redirect to filtered objects list after bulk import
|
||||
* [#10374](https://github.com/netbox-community/netbox/issues/10374) - Require unique tenant names & slugs per group
|
||||
* [#10729](https://github.com/netbox-community/netbox/issues/10729) - Add date & time custom field type
|
||||
* [#11029](https://github.com/netbox-community/netbox/issues/11029) - Enable change logging for cable terminations
|
||||
* [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging
|
||||
* [#11255](https://github.com/netbox-community/netbox/issues/11255) - Introduce the `scheduling_enabled` settings for reports & scripts
|
||||
* [#11291](https://github.com/netbox-community/netbox/issues/11291) - Optimized GraphQL API request handling
|
||||
* [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces
|
||||
* [#11494](https://github.com/netbox-community/netbox/issues/11494) - Enable filtering objects by create/update request IDs
|
||||
* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI
|
||||
* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments
|
||||
* [#11625](https://github.com/netbox-community/netbox/issues/11625) - Add HTMX support to ObjectEditView
|
||||
* [#11693](https://github.com/netbox-community/netbox/issues/11693) - Enable syncing export template content from remote sources
|
||||
* [#11780](https://github.com/netbox-community/netbox/issues/11780) - Enable loading import data from remote sources
|
||||
* [#11790](https://github.com/netbox-community/netbox/issues/11790) - Create database indexes for all generic foreign keys
|
||||
* [#11968](https://github.com/netbox-community/netbox/issues/11968) - Add navigation menu buttons to create device & VM components
|
||||
* [#12068](https://github.com/netbox-community/netbox/issues/12068) - Enable generic foreign key relationships from jobs to NetBox objects
|
||||
* [#12085](https://github.com/netbox-community/netbox/issues/12085) - Add a file source view for reports
|
||||
* [#12218](https://github.com/netbox-community/netbox/issues/12218) - Provide more relevant API endpoint descriptions in schema
|
||||
* [#12343](https://github.com/netbox-community/netbox/issues/12343) - Enforce a minimum length for `SECRET_KEY` configuration parameter
|
||||
|
||||
### Bug Fixes (From Beta2)
|
||||
|
||||
* [#12149](https://github.com/netbox-community/netbox/issues/12149) - Fix OpenAPI schema warnings relating to enum collisions
|
||||
* [#12195](https://github.com/netbox-community/netbox/issues/12195) - Fix exception when setting IP address role to null via REST API
|
||||
* [#12256](https://github.com/netbox-community/netbox/issues/12256) - Fix OpenAPI schema warnings relating to nested serializers
|
||||
* [#12278](https://github.com/netbox-community/netbox/issues/12278) - Fix schema warnings related to IPAddressField
|
||||
* [#12288](https://github.com/netbox-community/netbox/issues/12288) - Include `servers` definition in OpenAPI spec
|
||||
* [#12299](https://github.com/netbox-community/netbox/issues/12299) - Fix object list widget support for filtering by multiple values
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#9608](https://github.com/netbox-community/netbox/issues/9608) - Upgrade REST API schema to OpenAPI 3.0
|
||||
* [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template
|
||||
* [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`)
|
||||
* [#11489](https://github.com/netbox-community/netbox/issues/11489) - Consolidated several middleware classes
|
||||
* [#11611](https://github.com/netbox-community/netbox/issues/11611) - Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet
|
||||
* [#11694](https://github.com/netbox-community/netbox/issues/11694) - Remove obsolete `SmallTextarea` form widget
|
||||
* [#11737](https://github.com/netbox-community/netbox/issues/11737) - `ChangeLoggedModel` now inherits `WebhooksMixin`
|
||||
* [#11765](https://github.com/netbox-community/netbox/issues/11765) - Retire the `StaticSelect` and `StaticSelectMultiple` form widgets
|
||||
* [#11955](https://github.com/netbox-community/netbox/issues/11955) - Remove the unused `CSVDataField` and `CSVFileField` classes
|
||||
* [#12067](https://github.com/netbox-community/netbox/issues/12067) - Move & rename `extras.JobResult` to `core.Job`
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* All API responses now include a `X-Request-ID` HTTP header indicating the request's unique ID
|
||||
* Introduced new endpoints:
|
||||
* `/api/circuits/provider-accounts/`
|
||||
* `/api/core/data-files/`
|
||||
* `/api/core/data-sources/`
|
||||
* `/api/dcim/device/<id>/render-config/`
|
||||
* `/api/extras/config-templates/`
|
||||
* `/api/ipam/asn-ranges/`
|
||||
* Removed existing endpoints:
|
||||
* `/api/dcim/device/<id>/napalm/`
|
||||
* circuits.Circuit
|
||||
* Added the optional `account` foreign key to ProviderAccount
|
||||
* circuits.Provider
|
||||
* Removed the `account` field
|
||||
* dcim.CableTermination
|
||||
* Added `default_platform` foreign key (optional)
|
||||
* dcim.DeviceType
|
||||
* Added `default_platform` foreign key (optional)
|
||||
* dcim.InterfaceTemplate
|
||||
* Added `enabled` boolean field
|
||||
* Added optional `bridge` foreign key (optional)
|
||||
* extras.ConfigContext
|
||||
* Added `data_source`, `data_file`, `data_path`, and `data_synced` fields to enable syncing data from remote sources
|
||||
* extras.ExportTemplate
|
||||
* Added `data_source`, `data_file`, `data_path`, and `data_synced` fields to enable syncing content from remote sources
|
||||
* extras.Webhook
|
||||
* Added `type_job_start` and `type_job_end` boolean fields
|
||||
* ipam.ASN
|
||||
* The `rir` field now fully represents the assigned RIR (if any)
|
||||
* ipam.IPRange
|
||||
* Added the `mark_utilized` boolean field (default: false)
|
15
mkdocs.yml
15
mkdocs.yml
@ -78,8 +78,11 @@ nav:
|
||||
- Contacts: 'features/contacts.md'
|
||||
- Search: 'features/search.md'
|
||||
- Context Data: 'features/context-data.md'
|
||||
- Configuration Rendering: 'features/configuration-rendering.md'
|
||||
- Synchronized Data: 'features/synchronized-data.md'
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
- Background Jobs: 'features/background-jobs.md'
|
||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||
- API & Integration: 'features/api-integration.md'
|
||||
- Customization: 'features/customization.md'
|
||||
@ -105,7 +108,6 @@ nav:
|
||||
- Default Values: 'configuration/default-values.md'
|
||||
- Error Reporting: 'configuration/error-reporting.md'
|
||||
- Plugins: 'configuration/plugins.md'
|
||||
- NAPALM: 'configuration/napalm.md'
|
||||
- Date & Time: 'configuration/date-time.md'
|
||||
- Miscellaneous: 'configuration/miscellaneous.md'
|
||||
- Development: 'configuration/development.md'
|
||||
@ -120,7 +122,7 @@ nav:
|
||||
- REST API: 'integrations/rest-api.md'
|
||||
- GraphQL API: 'integrations/graphql-api.md'
|
||||
- Webhooks: 'integrations/webhooks.md'
|
||||
- NAPALM: 'integrations/napalm.md'
|
||||
- Synchronized Data: 'integrations/synchronized-data.md'
|
||||
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
|
||||
- Plugins:
|
||||
- Using Plugins: 'plugins/index.md'
|
||||
@ -137,6 +139,7 @@ nav:
|
||||
- REST API: 'plugins/development/rest-api.md'
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
|
||||
- Staged Changes: 'plugins/development/staged-changes.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
- Administration:
|
||||
@ -155,7 +158,12 @@ nav:
|
||||
- Circuit Termination: 'models/circuits/circuittermination.md'
|
||||
- Circuit Type: 'models/circuits/circuittype.md'
|
||||
- Provider: 'models/circuits/provider.md'
|
||||
- Provider Account: 'models/circuits/provideraccount.md'
|
||||
- Provider Network: 'models/circuits/providernetwork.md'
|
||||
- Core:
|
||||
- DataFile: 'models/core/datafile.md'
|
||||
- DataSource: 'models/core/datasource.md'
|
||||
- Job: 'models/core/job.md'
|
||||
- DCIM:
|
||||
- Cable: 'models/dcim/cable.md'
|
||||
- ConsolePort: 'models/dcim/consoleport.md'
|
||||
@ -200,6 +208,7 @@ nav:
|
||||
- Extras:
|
||||
- Branch: 'models/extras/branch.md'
|
||||
- ConfigContext: 'models/extras/configcontext.md'
|
||||
- ConfigTemplate: 'models/extras/configtemplate.md'
|
||||
- CustomField: 'models/extras/customfield.md'
|
||||
- CustomLink: 'models/extras/customlink.md'
|
||||
- ExportTemplate: 'models/extras/exporttemplate.md'
|
||||
@ -211,6 +220,7 @@ nav:
|
||||
- Webhook: 'models/extras/webhook.md'
|
||||
- IPAM:
|
||||
- ASN: 'models/ipam/asn.md'
|
||||
- ASNRange: 'models/ipam/asnrange.md'
|
||||
- Aggregate: 'models/ipam/aggregate.md'
|
||||
- FHRPGroup: 'models/ipam/fhrpgroup.md'
|
||||
- FHRPGroupAssignment: 'models/ipam/fhrpgroupassignment.md'
|
||||
@ -263,6 +273,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 3.5: 'release-notes/version-3.5.md'
|
||||
- Version 3.4: 'release-notes/version-3.4.md'
|
||||
- Version 3.3: 'release-notes/version-3.3.md'
|
||||
- Version 3.2: 'release-notes/version-3.2.md'
|
||||
|
@ -1,3 +1,5 @@
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
@ -9,6 +11,7 @@ __all__ = [
|
||||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderNetworkSerializer',
|
||||
'NestedProviderSerializer',
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
|
||||
@ -28,6 +31,9 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
# Providers
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
@ -37,10 +43,25 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display', 'name', 'account']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -18,6 +18,12 @@ from .nested_serializers import *
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
@ -31,11 +37,27 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
@ -70,8 +92,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = NestedSiteSerializer()
|
||||
provider_network = NestedProviderNetworkSerializer()
|
||||
site = NestedSiteSerializer(allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@ -84,18 +106,19 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
provider_account = NestedProviderAccountSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
@ -7,14 +7,13 @@ router.APIRootView = views.CircuitsRootView
|
||||
|
||||
# Providers
|
||||
router.register('providers', views.ProviderViewSet)
|
||||
router.register('provider-accounts', views.ProviderAccountViewSet)
|
||||
router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
|
||||
# Circuits
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
# Provider networks
|
||||
router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -46,7 +46,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||
|
||||
class CircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
@ -65,6 +65,16 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
#
|
||||
# Provider accounts
|
||||
#
|
||||
|
||||
class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
|
||||
serializer_class = serializers.ProviderAccountSerializer
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
@ -25,6 +25,22 @@ class CircuitStatusChoices(ChoiceSet):
|
||||
]
|
||||
|
||||
|
||||
class CircuitCommitRateChoices(ChoiceSet):
|
||||
key = 'Circuit.commit_rate'
|
||||
|
||||
CHOICES = [
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# CircuitTerminations
|
||||
#
|
||||
@ -38,3 +54,19 @@ class CircuitTerminationSideChoices(ChoiceSet):
|
||||
(SIDE_A, 'A'),
|
||||
(SIDE_Z, 'Z')
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
||||
key = 'CircuitTermination.port_speed'
|
||||
|
||||
CHOICES = [
|
||||
(10000, '10 Mbps'),
|
||||
(100000, '100 Mbps'),
|
||||
(1000000, '1 Gbps'),
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
@ -16,6 +16,7 @@ __all__ = (
|
||||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderAccountFilterSet',
|
||||
'ProviderFilterSet',
|
||||
)
|
||||
|
||||
@ -66,7 +67,34 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'account']
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(accounts__account__icontains=value) |
|
||||
Q(accounts__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'name', 'account', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -75,7 +103,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
@ -123,6 +151,11 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('ProviderAccount (ID)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
|
@ -1,20 +1,20 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
StaticSelect,
|
||||
)
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderAccountBulkEditForm',
|
||||
'ProviderNetworkBulkEditForm',
|
||||
)
|
||||
|
||||
@ -25,11 +25,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label=_('Account number')
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
@ -40,10 +35,32 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asns', 'account', )),
|
||||
(None, ('asns', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asns', 'account', 'description', 'comments',
|
||||
'asns', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('provider', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@ -96,11 +113,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider': '$provider'
|
||||
}
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
initial=''
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -116,7 +139,10 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Commit rate (Kbps)')
|
||||
label=_('Commit rate (Kbps)'),
|
||||
widget=NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
@ -129,7 +155,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'type', 'status', 'description')),
|
||||
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
|
||||
('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
|
||||
('Tenancy', ('tenant',)),
|
||||
)
|
||||
nullable_fields = (
|
||||
|
@ -6,13 +6,15 @@ from dcim.models import Site
|
||||
from django.utils.translation import gettext as _
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import BootstrapMixin, CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
'CircuitTerminationImportForm',
|
||||
'CircuitTypeImportForm',
|
||||
'ProviderImportForm',
|
||||
'ProviderAccountImportForm',
|
||||
'ProviderNetworkImportForm',
|
||||
)
|
||||
|
||||
@ -23,7 +25,21 @@ class ProviderImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = (
|
||||
'name', 'slug', 'account', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountImportForm(NetBoxModelImportForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = (
|
||||
'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@ -47,9 +63,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'description', 'tags')
|
||||
help_texts = {
|
||||
'name': _('Name of circuit type'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitImportForm(NetBoxModelImportForm):
|
||||
@ -58,6 +71,11 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
provider_account = CSVModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned provider account')
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
@ -77,8 +95,8 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'comments', 'tags'
|
||||
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,18 +1,20 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
'ProviderAccountFilterForm',
|
||||
'ProviderNetworkFilterForm',
|
||||
)
|
||||
|
||||
@ -56,6 +58,23 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('provider_id', 'account')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
account = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
@ -83,7 +102,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Provider', ('provider_id', 'provider_network_id')),
|
||||
('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')),
|
||||
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
@ -99,6 +118,14 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_account_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider account')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
@ -107,7 +134,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False
|
||||
)
|
||||
@ -141,6 +168,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label=_('Commit rate (Kbps)')
|
||||
label=_('Commit rate (Kbps)'),
|
||||
widget=NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
@ -1,20 +1,20 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField,
|
||||
StaticSelect,
|
||||
)
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
'ProviderAccountForm',
|
||||
'ProviderNetworkForm',
|
||||
)
|
||||
|
||||
@ -30,17 +30,26 @@ class ProviderForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
|
||||
('Support Info', ('account',)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'asns', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ProviderAccountForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'name': _("Full name of the provider"),
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkForm(NetBoxModelForm):
|
||||
@ -78,7 +87,15 @@ class CircuitTypeForm(NetBoxModelForm):
|
||||
|
||||
class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider',
|
||||
}
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
@ -86,7 +103,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'description', 'tags')),
|
||||
('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
|
||||
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
@ -94,87 +111,45 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'tenant_group', 'tenant', 'comments', 'tags',
|
||||
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'cid': _("Unique circuit ID"),
|
||||
'commit_rate': _("Committed rate"),
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
'install_date': DatePicker(),
|
||||
'termination_date': DatePicker(),
|
||||
'commit_rate': SelectSpeedWidget(),
|
||||
'commit_rate': NumberWithOptions(
|
||||
options=CircuitCommitRateChoices
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'circuits': '$circuit'
|
||||
}
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
query_params={
|
||||
'provider_id': '$provider',
|
||||
},
|
||||
)
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
required=False
|
||||
)
|
||||
provider_network_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label='Provider',
|
||||
initial_params={
|
||||
'networks': 'provider_network'
|
||||
}
|
||||
selector=True
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
query_params={
|
||||
'provider_id': '$provider_network_provider',
|
||||
},
|
||||
required=False
|
||||
required=False,
|
||||
selector=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network_provider',
|
||||
'provider_network', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'tags',
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': _("Physical circuit speed"),
|
||||
'xconnect_id': _("ID of the local cross-connect"),
|
||||
'pp_info': _("Patch panel ID and port number(s)")
|
||||
}
|
||||
widgets = {
|
||||
'term_side': StaticSelect(),
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
'upstream_speed': SelectSpeedWidget(),
|
||||
'port_speed': NumberWithOptions(
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
'upstream_speed': NumberWithOptions(
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
}
|
||||
|
@ -1,21 +1,41 @@
|
||||
import graphene
|
||||
|
||||
from circuits import models
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
|
||||
|
||||
class CircuitsQuery(graphene.ObjectType):
|
||||
circuit = ObjectField(CircuitType)
|
||||
circuit_list = ObjectListField(CircuitType)
|
||||
|
||||
def resolve_circuit_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Circuit.objects.all(), info)
|
||||
|
||||
circuit_termination = ObjectField(CircuitTerminationType)
|
||||
circuit_termination_list = ObjectListField(CircuitTerminationType)
|
||||
|
||||
def resolve_circuit_termination_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitTermination.objects.all(), info)
|
||||
|
||||
circuit_type = ObjectField(CircuitTypeType)
|
||||
circuit_type_list = ObjectListField(CircuitTypeType)
|
||||
|
||||
def resolve_circuit_type_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.CircuitType.objects.all(), info)
|
||||
|
||||
provider = ObjectField(ProviderType)
|
||||
provider_list = ObjectListField(ProviderType)
|
||||
|
||||
def resolve_provider_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Provider.objects.all(), info)
|
||||
|
||||
provider_account = ObjectField(ProviderAccountType)
|
||||
provider_account_list = ObjectListField(ProviderAccountType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_network_list = ObjectListField(ProviderNetworkType)
|
||||
|
||||
def resolve_provider_network_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.ProviderNetwork.objects.all(), info)
|
||||
|
@ -10,6 +10,7 @@ __all__ = (
|
||||
'CircuitType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
'ProviderNetworkType',
|
||||
)
|
||||
|
||||
@ -45,6 +46,14 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderAccount
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
|
@ -1,4 +1,4 @@
|
||||
import dcim.fields
|
||||
import ipam.fields
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@ -77,7 +77,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('asn', dcim.fields.ASNField(blank=True, null=True)),
|
||||
('asn', ipam.fields.ASNField(blank=True, null=True)),
|
||||
('account', models.CharField(blank=True, max_length=30)),
|
||||
('portal_url', models.URLField(blank=True)),
|
||||
('noc_contact', models.TextField(blank=True)),
|
||||
|
91
netbox/circuits/migrations/0042_provideraccount.py
Normal file
91
netbox/circuits/migrations/0042_provideraccount.py
Normal file
@ -0,0 +1,91 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
def create_provideraccounts_from_providers(apps, schema_editor):
|
||||
"""
|
||||
Migrate Account in Provider model to separate account model
|
||||
"""
|
||||
Provider = apps.get_model('circuits', 'Provider')
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
|
||||
provider_accounts = []
|
||||
for provider in Provider.objects.all():
|
||||
if provider.account:
|
||||
provider_accounts.append(ProviderAccount(
|
||||
provider=provider,
|
||||
account=provider.account
|
||||
))
|
||||
ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100)
|
||||
|
||||
|
||||
def restore_providers_from_provideraccounts(apps, schema_editor):
|
||||
"""
|
||||
Restore Provider account values from auto-generated ProviderAccounts
|
||||
"""
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
provider_accounts = ProviderAccount.objects.order_by('pk')
|
||||
for provideraccount in provider_accounts:
|
||||
if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount:
|
||||
provideraccount.provider.account = provideraccount.account
|
||||
provideraccount.provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('circuits', '0041_standardize_description_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProviderAccount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('account', models.CharField(max_length=100)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('provider', 'account'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
create_provideraccounts_from_providers, restore_providers_from_provideraccounts
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider_account',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='circuit',
|
||||
options={'ordering': ['provider', 'provider_account', 'cid']},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
|
||||
),
|
||||
]
|
@ -1,4 +1,3 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
@ -10,7 +9,6 @@ from dcim.models import CabledObjectModel
|
||||
from netbox.models import (
|
||||
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin,
|
||||
)
|
||||
from netbox.models.features import WebhooksMixin
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@ -31,18 +29,26 @@ class CircuitType(OrganizationalModel):
|
||||
class Circuit(PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
|
||||
in Kbps.
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
|
||||
ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
|
||||
"""
|
||||
cid = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name='Circuit ID'
|
||||
verbose_name='Circuit ID',
|
||||
help_text=_("Unique circuit ID")
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
provider_account = models.ForeignKey(
|
||||
to='circuits.ProviderAccount',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='CircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
@ -73,7 +79,9 @@ class Circuit(PrimaryModel):
|
||||
commit_rate = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Commit rate (Kbps)')
|
||||
verbose_name='Commit rate (Kbps)',
|
||||
help_text=_("Committed rate")
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
@ -102,7 +110,8 @@ class Circuit(PrimaryModel):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.CircuitType',
|
||||
@ -110,12 +119,16 @@ class Circuit(PrimaryModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
ordering = ['provider', 'provider_account', 'cid']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_cid'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@ -127,12 +140,17 @@ class Circuit(PrimaryModel):
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.provider_account and self.provider != self.provider_account.provider:
|
||||
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
|
||||
|
||||
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
TagsMixin,
|
||||
WebhooksMixin,
|
||||
ChangeLoggedModel,
|
||||
CabledObjectModel
|
||||
):
|
||||
@ -163,7 +181,8 @@ class CircuitTermination(
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name='Port speed (Kbps)',
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text=_("Physical circuit speed")
|
||||
)
|
||||
upstream_speed = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
@ -174,12 +193,14 @@ class CircuitTermination(
|
||||
xconnect_id = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name='Cross-connect ID'
|
||||
verbose_name='Cross-connect ID',
|
||||
help_text=_("ID of the local cross-connect")
|
||||
)
|
||||
pp_info = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
verbose_name='Patch panel/port(s)'
|
||||
verbose_name='Patch panel/port(s)',
|
||||
help_text=_("Patch panel ID and port number(s)")
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
|
@ -1,12 +1,15 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
|
||||
__all__ = (
|
||||
'ProviderNetwork',
|
||||
'Provider',
|
||||
'ProviderAccount',
|
||||
)
|
||||
|
||||
|
||||
@ -17,7 +20,8 @@ class Provider(PrimaryModel):
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
unique=True,
|
||||
help_text=_("Full name of the provider")
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
@ -28,20 +32,13 @@ class Provider(PrimaryModel):
|
||||
related_name='providers',
|
||||
blank=True
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
verbose_name='Account number'
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'account',
|
||||
)
|
||||
clone_fields = ()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -53,6 +50,54 @@ class Provider(PrimaryModel):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderAccount(PrimaryModel):
|
||||
"""
|
||||
This is a discrete account within a provider. Each Circuit belongs to a Provider Account.
|
||||
"""
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='accounts'
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name='Account ID'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = ('provider', )
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'account')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'account'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_account'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_name',
|
||||
condition=~Q(name="")
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return f'{self.account} ({self.name})'
|
||||
return f'{self.account}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provideraccount', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
|
@ -39,12 +39,20 @@ class ProviderIndex(SearchIndex):
|
||||
model = models.Provider
|
||||
fields = (
|
||||
('name', 100),
|
||||
('account', 200),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountIndex(SearchIndex):
|
||||
model = models.ProviderAccount
|
||||
fields = (
|
||||
('name', 100),
|
||||
('account', 200),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
||||
|
||||
@register_search
|
||||
class ProviderNetworkIndex(SearchIndex):
|
||||
model = models.ProviderNetwork
|
||||
|
@ -1,4 +1,5 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from circuits.models import *
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
@ -28,7 +29,9 @@ class CircuitTypeTable(NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuittype_list'
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
circuit_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:circuit_list',
|
||||
url_params={'type_id': 'pk'},
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
|
||||
@ -48,6 +51,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Account'
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
@ -68,9 +75,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
|
||||
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
@ -7,6 +7,7 @@ from netbox.tables import NetBoxTable, columns
|
||||
|
||||
__all__ = (
|
||||
'ProviderTable',
|
||||
'ProviderAccountTable',
|
||||
'ProviderNetworkTable',
|
||||
)
|
||||
|
||||
@ -15,6 +16,16 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
accounts = columns.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='Accounts'
|
||||
)
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'account_id': 'pk'},
|
||||
verbose_name='Account Count'
|
||||
)
|
||||
asns = columns.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='ASNs'
|
||||
@ -39,10 +50,38 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
|
||||
'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'account', 'circuit_count')
|
||||
default_columns = ('pk', 'name', 'account_count', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
|
||||
account = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column()
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
circuit_count = columns.LinkedCountColumn(
|
||||
accessor=Accessor('count_circuits'),
|
||||
viewname='circuits:circuit_list',
|
||||
url_params={'provider_account_id': 'pk'},
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:provideraccount_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ProviderAccount
|
||||
fields = (
|
||||
'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderNetworkTable(NetBoxTable):
|
||||
|
@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'account': '1234',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -106,6 +106,12 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
@ -113,9 +119,9 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@ -123,16 +129,19 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'cid': 'Circuit 4',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
{
|
||||
'cid': 'Circuit 5',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
{
|
||||
'cid': 'Circuit 6',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
]
|
||||
@ -197,6 +206,49 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'display', 'id', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Provider Account 4',
|
||||
'provider': providers[0].pk,
|
||||
'account': '4567',
|
||||
},
|
||||
{
|
||||
'name': 'Provider Account 5',
|
||||
'provider': providers[0].pk,
|
||||
'account': '5678',
|
||||
},
|
||||
{
|
||||
'name': 'Provider Account 6',
|
||||
'provider': providers[0].pk,
|
||||
'account': '6789',
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
|
@ -25,11 +25,11 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', account='2345'),
|
||||
Provider(name='Provider 3', slug='provider-3', account='3456'),
|
||||
Provider(name='Provider 4', slug='provider-4', account='4567'),
|
||||
Provider(name='Provider 5', slug='provider-5', account='5678'),
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
Provider(name='Provider 5', slug='provider-5'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0]])
|
||||
@ -64,8 +64,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Circuit 2'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@ -87,10 +87,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@ -193,9 +189,17 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
@ -204,12 +208,12 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@ -246,6 +250,11 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'provider': [provider.slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
@ -445,3 +454,44 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
filterset = ProviderAccountFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], description='foobar1', account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], description='foobar2', account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[2], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Account 1', 'Provider Account 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '3456']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -38,7 +38,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'name': 'Provider X',
|
||||
'slug': 'provider-x',
|
||||
'asns': [asns[6].pk, asns[7].pk],
|
||||
'account': '1234',
|
||||
'comments': 'Another provider',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@ -58,7 +57,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'account': '5678',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@ -124,6 +122,12 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
circuittypes = (
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
@ -131,9 +135,9 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
CircuitType.objects.bulk_create(circuittypes)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
)
|
||||
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
@ -143,6 +147,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'cid': 'Circuit X',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuittypes[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'tenant': None,
|
||||
@ -155,10 +160,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider,type,status",
|
||||
"Circuit 4,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 5,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 6,Provider 1,Circuit Type 1,active",
|
||||
"cid,provider,provider_account,type,status",
|
||||
"Circuit 4,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
"Circuit 5,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
"Circuit 6,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
@ -170,6 +175,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuittypes[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'tenant': None,
|
||||
@ -179,6 +185,57 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderAccount
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Provider Account X',
|
||||
'provider': providers[1].pk,
|
||||
'account': 'XXXX',
|
||||
'description': 'A new provider network',
|
||||
'comments': 'Longer description goes here',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,provider,account,description",
|
||||
"Provider Account 4,Provider 1,4567,Foo",
|
||||
"Provider Account 5,Provider 1,5678,Bar",
|
||||
"Provider Account 6,Provider 1,6789,Baz",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,account,description",
|
||||
f"{provider_accounts[0].pk},Provider Network 7,7890,New description7",
|
||||
f"{provider_accounts[1].pk},Provider Network 8,8901,New description8",
|
||||
f"{provider_accounts[2].pk},Provider Network 9,9012,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderNetwork
|
||||
|
||||
|
@ -14,6 +14,14 @@ urlpatterns = [
|
||||
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
|
||||
|
||||
# Provider accounts
|
||||
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
|
||||
path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'),
|
||||
path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'),
|
||||
path('provider-accounts/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'),
|
||||
path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'),
|
||||
path('provider-accounts/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
|
||||
|
||||
# Provider networks
|
||||
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
|
||||
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
|
||||
|
@ -30,17 +30,13 @@ class ProviderView(generic.ObjectView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
provider=instance
|
||||
).prefetch_related(
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
related_models = (
|
||||
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
||||
circuits_table.configure(request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -58,7 +54,6 @@ class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
class ProviderBulkImportView(generic.BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderImportForm
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
class ProviderBulkEditView(generic.BulkEditView):
|
||||
@ -78,6 +73,67 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
#
|
||||
# ProviderAccounts
|
||||
#
|
||||
|
||||
class ProviderAccountListView(generic.ObjectListView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
filterset_form = forms.ProviderAccountFilterForm
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount)
|
||||
class ProviderAccountView(generic.ObjectView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'edit')
|
||||
class ProviderAccountEditView(generic.ObjectEditView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
form = forms.ProviderAccountForm
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'delete')
|
||||
class ProviderAccountDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
|
||||
class ProviderAccountBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
model_form = forms.ProviderAccountImportForm
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
table = tables.ProviderAccountTable
|
||||
form = forms.ProviderAccountBulkEditForm
|
||||
|
||||
|
||||
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
@ -94,18 +150,15 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
Q(termination_a__provider_network=instance.pk) |
|
||||
Q(termination_z__provider_network=instance.pk)
|
||||
).prefetch_related(
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
related_models = (
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'providernetwork_id',
|
||||
),
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits, user=request.user)
|
||||
circuits_table.configure(request)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -123,7 +176,6 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkImportForm
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
@ -157,12 +209,12 @@ class CircuitTypeView(generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
||||
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',))
|
||||
circuits_table.configure(request)
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'circuits_table': circuits_table,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -180,7 +232,6 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeImportForm
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
@ -233,7 +284,6 @@ class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
class CircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitImportForm
|
||||
table = tables.CircuitTable
|
||||
additional_permissions = [
|
||||
'circuits.add_circuittermination',
|
||||
]
|
||||
|
0
netbox/core/api/__init__.py
Normal file
0
netbox/core/api/__init__.py
Normal file
41
netbox/core/api/nested_serializers.py
Normal file
41
netbox/core/api/nested_serializers.py
Normal file
@ -0,0 +1,41 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedDataFileSerializer',
|
||||
'NestedDataSourceSerializer',
|
||||
'NestedJobSerializer',
|
||||
)
|
||||
|
||||
|
||||
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDataFileSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = ['id', 'url', 'display', 'path']
|
||||
|
||||
|
||||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ['url', 'created', 'completed', 'user', 'status']
|
263
netbox/core/api/schema.py
Normal file
263
netbox/core/api/schema.py
Normal file
@ -0,0 +1,263 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from drf_spectacular.extensions import (
|
||||
OpenApiSerializerFieldExtension,
|
||||
OpenApiViewExtension,
|
||||
)
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import (
|
||||
ComponentRegistry,
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_choice_field,
|
||||
build_media_type_object,
|
||||
build_object_type,
|
||||
get_doc,
|
||||
is_serializer,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
|
||||
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
|
||||
|
||||
|
||||
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
|
||||
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
return build_basic_type(OpenApiTypes.STR)
|
||||
|
||||
|
||||
class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
target_class = 'netbox.api.fields.ChoiceField'
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
if direction == 'request':
|
||||
return build_choice_field(self.target)
|
||||
|
||||
elif direction == "response":
|
||||
return build_object_type(
|
||||
properties={
|
||||
"value": build_basic_type(OpenApiTypes.STR),
|
||||
"label": build_basic_type(OpenApiTypes.STR),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class NetBoxAutoSchema(AutoSchema):
|
||||
"""
|
||||
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
||||
1. bulk serializers cause operation_id conflicts with non-bulk ones
|
||||
2. bulk operations should specify a list
|
||||
3. bulk operations don't have filter params
|
||||
4. bulk operations don't have pagination
|
||||
5. bulk delete should specify input
|
||||
"""
|
||||
|
||||
writable_serializers = {}
|
||||
|
||||
@property
|
||||
def is_bulk_action(self):
|
||||
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_operation_id(self):
|
||||
"""
|
||||
bulk serializers cause operation_id conflicts with non-bulk ones
|
||||
bulk operations cause id conflicts in spectacular resulting in numerous:
|
||||
Warning: operationId "xxx" has collisions [xxx]. "resolving with numeral suffixes"
|
||||
code is modified from drf_spectacular.openapi.AutoSchema.get_operation_id
|
||||
"""
|
||||
if self.is_bulk_action:
|
||||
tokenized_path = self._tokenize_path()
|
||||
# replace dashes as they can be problematic later in code generation
|
||||
tokenized_path = [t.replace('-', '_') for t in tokenized_path]
|
||||
|
||||
if self.method == 'GET' and self._is_list_view():
|
||||
# this shouldn't happen, but keeping it here to follow base code
|
||||
action = 'list'
|
||||
else:
|
||||
# action = self.method_mapping[self.method.lower()]
|
||||
# use bulk name so partial_update -> bulk_partial_update
|
||||
action = self.view.action.lower()
|
||||
|
||||
if not tokenized_path:
|
||||
tokenized_path.append('root')
|
||||
|
||||
if re.search(r'<drf_format_suffix\w*:\w+>', self.path_regex):
|
||||
tokenized_path.append('formatted')
|
||||
|
||||
return '_'.join(tokenized_path + [action])
|
||||
|
||||
# if not bulk - just return normal id
|
||||
return super().get_operation_id()
|
||||
|
||||
def get_request_serializer(self) -> typing.Any:
|
||||
# bulk operations should specify a list
|
||||
serializer = super().get_request_serializer()
|
||||
|
||||
if self.is_bulk_action:
|
||||
return type(serializer)(many=True)
|
||||
|
||||
# handle mapping for Writable serializers - adapted from dansheps original code
|
||||
# for drf-yasg
|
||||
if serializer is not None and self.method in WRITABLE_ACTIONS:
|
||||
writable_class = self.get_writable_class(serializer)
|
||||
if writable_class is not None:
|
||||
if hasattr(serializer, "child"):
|
||||
child_serializer = self.get_writable_class(serializer.child)
|
||||
serializer = writable_class(context=serializer.context, child=child_serializer)
|
||||
else:
|
||||
serializer = writable_class(context=serializer.context)
|
||||
|
||||
return serializer
|
||||
|
||||
def get_response_serializers(self) -> typing.Any:
|
||||
# bulk operations should specify a list
|
||||
response_serializers = super().get_response_serializers()
|
||||
|
||||
if self.is_bulk_action:
|
||||
return type(response_serializers)(many=True)
|
||||
|
||||
return response_serializers
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
"""
|
||||
serializer_meta = getattr(serializer, 'Meta', None)
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
ref_name = ref_name[: -len('Serializer')]
|
||||
return ref_name
|
||||
|
||||
def get_writable_class(self, serializer):
|
||||
properties = {}
|
||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||
remove_fields = []
|
||||
|
||||
for child_name, child in fields.items():
|
||||
# read_only fields don't need to be in writable (write only) serializers
|
||||
if 'read_only' in dir(child) and child.read_only:
|
||||
remove_fields.append(child_name)
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
properties[child_name] = None
|
||||
|
||||
if not properties:
|
||||
return None
|
||||
|
||||
if type(serializer) not in self.writable_serializers:
|
||||
writable_name = 'Writable' + type(serializer).__name__
|
||||
meta_class = getattr(type(serializer), 'Meta', None)
|
||||
if meta_class:
|
||||
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
|
||||
# remove read_only fields from write-only serializers
|
||||
fields = list(meta_class.fields)
|
||||
for field in remove_fields:
|
||||
fields.remove(field)
|
||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name, 'fields': fields})
|
||||
|
||||
properties['Meta'] = writable_meta
|
||||
|
||||
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
||||
|
||||
writable_class = self.writable_serializers[type(serializer)]
|
||||
return writable_class
|
||||
|
||||
def get_filter_backends(self):
|
||||
# bulk operations don't have filter params
|
||||
if self.is_bulk_action:
|
||||
return []
|
||||
return super().get_filter_backends()
|
||||
|
||||
def _get_paginator(self):
|
||||
# bulk operations don't have pagination
|
||||
if self.is_bulk_action:
|
||||
return None
|
||||
return super()._get_paginator()
|
||||
|
||||
def _get_request_body(self, direction='request'):
|
||||
# bulk delete should specify input
|
||||
if (not self.is_bulk_action) or (self.method != 'DELETE'):
|
||||
return super()._get_request_body(direction)
|
||||
|
||||
# rest from drf_spectacular.openapi.AutoSchema._get_request_body
|
||||
# but remove the unsafe method check
|
||||
|
||||
request_serializer = self.get_request_serializer()
|
||||
|
||||
if isinstance(request_serializer, dict):
|
||||
content = []
|
||||
request_body_required = True
|
||||
for media_type, serializer in request_serializer.items():
|
||||
schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
|
||||
examples = self._get_examples(serializer, direction, media_type)
|
||||
if schema is None:
|
||||
continue
|
||||
content.append((media_type, schema, examples))
|
||||
request_body_required &= partial_request_body_required
|
||||
else:
|
||||
schema, request_body_required = self._get_request_for_media_type(request_serializer, direction)
|
||||
if schema is None:
|
||||
return None
|
||||
content = [
|
||||
(media_type, schema, self._get_examples(request_serializer, direction, media_type))
|
||||
for media_type in self.map_parsers()
|
||||
]
|
||||
|
||||
request_body = {
|
||||
'content': {
|
||||
media_type: build_media_type_object(schema, examples) for media_type, schema, examples in content
|
||||
}
|
||||
}
|
||||
if request_body_required:
|
||||
request_body['required'] = request_body_required
|
||||
return request_body
|
||||
|
||||
def get_description(self):
|
||||
"""
|
||||
Return a string description for the ViewSet.
|
||||
"""
|
||||
|
||||
# If a docstring is provided, use it.
|
||||
if self.view.__doc__:
|
||||
return get_doc(self.view.__class__)
|
||||
|
||||
# When the action method is decorated with @action, use the docstring of the method.
|
||||
action_or_method = getattr(self.view, getattr(self.view, 'action', self.method.lower()), None)
|
||||
if action_or_method and action_or_method.__doc__:
|
||||
return get_doc(action_or_method)
|
||||
|
||||
# Else, generate a description from the class name.
|
||||
return self._generate_description()
|
||||
|
||||
def _generate_description(self):
|
||||
"""
|
||||
Generate a docstring for the method. It also takes into account whether the method is for list or detail.
|
||||
"""
|
||||
model_name = self.view.queryset.model._meta.verbose_name
|
||||
|
||||
# Determine if the method is for list or detail.
|
||||
if '{id}' in self.path:
|
||||
return f"{self.method.capitalize()} a {model_name} object."
|
||||
return f"{self.method.capitalize()} a list of {model_name} objects."
|
72
netbox/core/api/serializers.py
Normal file
72
netbox/core/api/serializers.py
Normal file
@ -0,0 +1,72 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'DataFileSerializer',
|
||||
'DataSourceSerializer',
|
||||
'JobSerializer',
|
||||
)
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=DataSourceTypeChoices
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=DataSourceStatusChoices,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
file_count = serializers.IntegerField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'created', 'last_updated', 'file_count',
|
||||
]
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = NestedDataSourceSerializer(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
)
|
||||
status = ChoiceField(choices=JobStatusChoices, read_only=True)
|
||||
object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'job_id',
|
||||
]
|
16
netbox/core/api/urls.py
Normal file
16
netbox/core/api/urls.py
Normal file
@ -0,0 +1,16 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CoreRootView
|
||||
|
||||
# Data sources
|
||||
router.register('data-sources', views.DataSourceViewSet)
|
||||
router.register('data-files', views.DataFileViewSet)
|
||||
|
||||
# Jobs
|
||||
router.register('jobs', views.JobViewSet)
|
||||
|
||||
app_name = 'core-api'
|
||||
urlpatterns = router.urls
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user