mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
7b90481fc9
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -23,7 +23,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.6.9
|
||||
placeholder: v3.7.0
|
||||
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.6.9
|
||||
placeholder: v3.7.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -53,8 +53,7 @@ django-tables2
|
||||
|
||||
# User-defined tags for objects
|
||||
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
|
||||
# TODO: Upgrade to v5.0 for NetBox v3.7 beta
|
||||
django-taggit<5.0
|
||||
django-taggit
|
||||
|
||||
# A Django field for representing time zones
|
||||
# https://github.com/mfogel/django-timezone-field/
|
||||
@ -90,9 +89,8 @@ gunicorn
|
||||
Jinja2
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://python-markdown.github.io/change_log/
|
||||
# mkdocs currently requires Markdown v3.3
|
||||
Markdown<3.4
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
|
||||
# File inclusion plugin for Python-Markdown
|
||||
# https://github.com/cmacmackin/markdown-include
|
||||
@ -126,10 +124,6 @@ PyYAML
|
||||
# https://github.com/psf/requests/blob/main/HISTORY.md
|
||||
requests
|
||||
|
||||
# Sentry SDK
|
||||
# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md
|
||||
sentry-sdk
|
||||
|
||||
# Social authentication framework
|
||||
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
|
||||
social-auth-core
|
||||
|
@ -4,27 +4,15 @@
|
||||
|
||||
### Enabling Error Reporting
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### Using a Custom DSN
|
||||
|
||||
If you prefer instead to use your own Sentry ingestor, you'll need to first create a new project under your Sentry account to represent your NetBox deployment and obtain its corresponding data source name (DSN). This looks like a URL similar to the example below:
|
||||
|
||||
```
|
||||
https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
```
|
||||
|
||||
Once you have obtained a DSN, configure Sentry in NetBox's `configuration.py` file with the following parameters:
|
||||
NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, set `SENTRY_ENABLED` to True and define your unique [data source name (DSN)](https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/) in `configuration.py`.
|
||||
|
||||
```python
|
||||
SENTRY_ENABLED = True
|
||||
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||
```
|
||||
|
||||
Setting `SENTRY_ENABLED` to False will disable the Sentry integration.
|
||||
|
||||
### Assigning Tags
|
||||
|
||||
You can optionally attach one or more arbitrary tags to the outgoing error reports if desired by setting the `SENTRY_TAGS` parameter:
|
||||
|
@ -87,3 +87,24 @@ The following colors are supported:
|
||||
* `gray`
|
||||
* `black`
|
||||
* `white`
|
||||
|
||||
---
|
||||
|
||||
## PROTECTION_RULES
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) against which an object is evaluated immediately prior to its deletion. If validation fails, the object is not deleted. An example is provided below:
|
||||
|
||||
```python
|
||||
PROTECTION_RULES = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"status": {
|
||||
"eq": "decommissioning"
|
||||
}
|
||||
},
|
||||
"my_plugin.validators.Validator1",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
@ -18,6 +18,9 @@ Default: False
|
||||
|
||||
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
|
||||
|
||||
!!! note
|
||||
The `sentry-sdk` Python package is required to enable Sentry integration.
|
||||
|
||||
---
|
||||
|
||||
## SENTRY_SAMPLE_RATE
|
||||
|
@ -80,6 +80,17 @@ changes in the database indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_SKIP_EMPTY_CHANGES
|
||||
|
||||
Default: True
|
||||
|
||||
If enabled, a change log record will not be created when an object is updated without any changes to its existing field values.
|
||||
|
||||
!!! note
|
||||
The object's `last_updated` field will always reflect the time of the most recent update, regardless of this parameter.
|
||||
|
||||
---
|
||||
|
||||
## DATA_UPLOAD_MAX_MEMORY_SIZE
|
||||
|
||||
Default: `2621440` (2.5 MB)
|
||||
@ -92,9 +103,12 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: False
|
||||
Default: True
|
||||
|
||||
By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True.
|
||||
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
|
||||
|
||||
!!! info "Changed in v3.7"
|
||||
The default value for this parameter was changed from False to True in NetBox v3.7.
|
||||
|
||||
---
|
||||
|
||||
|
@ -59,10 +59,7 @@ DATABASE = {
|
||||
|
||||
## REDIS
|
||||
|
||||
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
|
||||
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
|
||||
functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
|
||||
task queuing and caching, allowing the user to connect to different Redis instances/databases per feature.
|
||||
[Redis](https://redis.io/) is a lightweight in-memory data store similar to memcached. NetBox employs Redis for background task queuing and other features.
|
||||
|
||||
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `tasks` and `caching` subsections:
|
||||
|
||||
@ -81,7 +78,7 @@ REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'USERNAME': 'netbox'
|
||||
'USERNAME': 'netbox',
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
@ -89,7 +86,7 @@ REDIS = {
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'USERNAME': ''
|
||||
'USERNAME': '',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
|
@ -40,14 +40,22 @@ Related custom fields can be grouped together within the UI by assigning each th
|
||||
|
||||
This parameter has no effect on the API representation of custom field data.
|
||||
|
||||
### Visibility
|
||||
### Visibility & Editing
|
||||
|
||||
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.
|
||||
!!! info "This feature was improved in NetBox v3.7."
|
||||
|
||||
* **Read/write** (default): The custom field is included when viewing and editing objects.
|
||||
* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.)
|
||||
When creating a custom field, users can control the conditions under which it may be displayed and edited within the NetBox user interface. The following choices are available for controlling the display of a custom field on an object:
|
||||
|
||||
* **Always** (default): The custom field is included when viewing an object.
|
||||
* **If Set**: The custom field is included only if a value has been defined for the object.
|
||||
* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users.
|
||||
|
||||
Additionally, the following options are available for controlling whether custom field values can be altered within the NetBox UI:
|
||||
|
||||
* **Yes** (default): The custom field's value may be modified when editing an object.
|
||||
* **No**: The custom field is displayed for reference when editing an object, but its value may not be modified.
|
||||
* **Hidden**: The custom field is not displayed when editing an object.
|
||||
|
||||
Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API.
|
||||
|
||||
### Validation
|
||||
|
@ -26,6 +26,8 @@ The `CustomValidator` class supports several validation types:
|
||||
* `regex`: Application of a [regular expression](https://en.wikipedia.org/wiki/Regular_expression)
|
||||
* `required`: A value must be specified
|
||||
* `prohibited`: A value must _not_ be specified
|
||||
* `eq`: A value must be equal to the specified value
|
||||
* `neq`: A value must _not_ be equal to the specified value
|
||||
|
||||
The `min` and `max` types should be defined for numeric values, whereas `min_length`, `max_length`, and `regex` are suitable for character strings (text values). The `required` and `prohibited` validators may be used for any field, and should be passed a value of `True`.
|
||||
|
||||
|
@ -31,7 +31,7 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
|
||||
'dcim': ['site', 'rack', 'devicetype', ...],
|
||||
...
|
||||
},
|
||||
'webhooks': {
|
||||
'event_rules': {
|
||||
'extras': ['configcontext', 'tag', ...],
|
||||
'dcim': ['site', 'rack', 'devicetype', ...],
|
||||
},
|
||||
@ -41,6 +41,10 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
|
||||
|
||||
Supported model features are listed in the [features matrix](./models.md#features-matrix).
|
||||
|
||||
### `models`
|
||||
|
||||
This key lists all models which have been registered in NetBox which are not designated for private use. (Setting `_netbox_private` to True on a model excludes it from this list.) As with individual features under `model_features`, models are organized by app label.
|
||||
|
||||
### `plugins`
|
||||
|
||||
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
|
||||
@ -49,6 +53,10 @@ This store maintains all registered items for plugins, such as navigation menus,
|
||||
|
||||
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
|
||||
|
||||
### `tables`
|
||||
|
||||
A dictionary mapping table classes to lists of extra columns that have been registered by plugins using the `register_table_column()` utility function. Each column is defined as a tuple of name and column instance.
|
||||
|
||||
### `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,12 +2,25 @@
|
||||
|
||||
Below is a list of tasks to consider when adding a new field to a core model.
|
||||
|
||||
## 1. Generate and run database migrations
|
||||
## 1. Add the field to the model class
|
||||
|
||||
Add the field to the model, taking care to address any of the following conditions.
|
||||
|
||||
* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example:
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = (
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
```
|
||||
|
||||
## 2. Generate and run database migrations
|
||||
|
||||
[Django migrations](https://docs.djangoproject.com/en/stable/topics/migrations/) are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
|
||||
|
||||
```
|
||||
./manage.py makemigrations <app> -n <name>
|
||||
./manage.py makemigrations <app> -n <name> --no-header
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
@ -16,7 +29,7 @@ Where possible, try to merge related changes into a single migration. For exampl
|
||||
!!! warning "Do not alter existing migrations"
|
||||
Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered (other than for the purpose of correcting a bug).
|
||||
|
||||
## 2. Add validation logic to `clean()`
|
||||
## 3. Add validation logic to `clean()`
|
||||
|
||||
If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or after your custom validation as appropriate:
|
||||
|
||||
@ -31,15 +44,15 @@ class Foo(models.Model):
|
||||
raise ValidationError()
|
||||
```
|
||||
|
||||
## 3. Update relevant querysets
|
||||
## 4. Update relevant querysets
|
||||
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retrieving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
|
||||
|
||||
## 4. Update API serializer
|
||||
## 5. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
|
||||
|
||||
## 5. Add fields to forms
|
||||
## 6. Add fields to forms
|
||||
|
||||
Extend any forms to include the new field(s) as appropriate. These are found under the `forms/` directory within each app. Common forms include:
|
||||
|
||||
@ -48,23 +61,23 @@ Extend any forms to include the new field(s) as appropriate. These are found und
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
## 6. Extend object filter set
|
||||
## 7. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to query it in the FilterSet's `search()` method.
|
||||
|
||||
## 7. Add column to object table
|
||||
## 8. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column. Also add the field name to `default_columns` if the column should be present in the table by default.
|
||||
|
||||
## 8. Update the SearchIndex
|
||||
## 9. Update the SearchIndex
|
||||
|
||||
Where applicable, add the new field to the model's SearchIndex for inclusion in global search.
|
||||
|
||||
## 9. Update the UI templates
|
||||
## 10. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
## 10. Create/extend test cases
|
||||
## 11. Create/extend test cases
|
||||
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
@ -74,8 +87,8 @@ Create or extend the relevant test cases to verify that the new field and any ac
|
||||
* Model tests
|
||||
* View tests
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
Be diligent to ensure all the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
||||
## 11. Update the model's documentation
|
||||
## 12. Update the model's documentation
|
||||
|
||||
Each model has a dedicated page in the documentation, at `models/<app>/<model>.md`. Update this file to include any relevant information about the new field.
|
||||
|
@ -10,19 +10,19 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
|
||||
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).
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| [Event rules](../features/event-rules.md) | `EventRulesMixin` | `event_rules` | Event rules can send webhooks or run custom scripts automatically in response to events |
|
||||
|
||||
## Models Index
|
||||
|
||||
@ -52,7 +52,6 @@ These are considered the "core" application models which are used to model netwo
|
||||
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
|
||||
* [ipam.IPAddress](../models/ipam/ipaddress.md)
|
||||
* [ipam.IPRange](../models/ipam/iprange.md)
|
||||
* [ipam.L2VPN](../models/ipam/l2vpn.md)
|
||||
* [ipam.Prefix](../models/ipam/prefix.md)
|
||||
* [ipam.RouteTarget](../models/ipam/routetarget.md)
|
||||
* [ipam.Service](../models/ipam/service.md)
|
||||
@ -63,6 +62,13 @@ These are considered the "core" application models which are used to model netwo
|
||||
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
||||
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
||||
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
||||
* [vpn.IKEPolicy](../models/vpn/ikepolicy.md)
|
||||
* [vpn.IKEProposal](../models/vpn/ikeproposal.md)
|
||||
* [vpn.IPSecPolicy](../models/vpn/ipsecpolicy.md)
|
||||
* [vpn.IPSecProfile](../models/vpn/ipsecprofile.md)
|
||||
* [vpn.IPSecProposal](../models/vpn/ipsecproposal.md)
|
||||
* [vpn.L2VPN](../models/vpn/l2vpn.md)
|
||||
* [vpn.Tunnel](../models/vpn/tunnel.md)
|
||||
* [wireless.WirelessLAN](../models/wireless/wirelesslan.md)
|
||||
* [wireless.WirelessLink](../models/wireless/wirelesslink.md)
|
||||
|
||||
@ -75,6 +81,7 @@ Organization models are used to organize and classify primary models.
|
||||
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
||||
* [dcim.Platform](../models/dcim/platform.md)
|
||||
* [dcim.RackRole](../models/dcim/rackrole.md)
|
||||
* [ipam.ASNRange](../models/ipam/asnrange.md)
|
||||
* [ipam.RIR](../models/ipam/rir.md)
|
||||
* [ipam.Role](../models/ipam/role.md)
|
||||
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
|
||||
@ -107,11 +114,12 @@ Component models represent individual physical or virtual components belonging t
|
||||
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
|
||||
* [dcim.PowerPort](../models/dcim/powerport.md)
|
||||
* [dcim.RearPort](../models/dcim/rearport.md)
|
||||
* [virtualization.VirtualDisk](../models/virtualization/virtualdisk.md)
|
||||
* [virtualization.VMInterface](../models/virtualization/vminterface.md)
|
||||
|
||||
### 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.
|
||||
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 event rules.
|
||||
|
||||
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
|
||||
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
|
||||
|
@ -17,6 +17,7 @@ class MyModelIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('site', 'device', 'status', 'description')
|
||||
```
|
||||
|
||||
A SearchIndex subclass defines both its model and a list of two-tuples specifying which model fields to be indexed and the weight (precedence) associated with each. Guidance on weight assignment for fields is provided below.
|
||||
|
@ -9,3 +9,27 @@ This signal is sent by models which inherit from `CustomValidationMixin` at the
|
||||
### Receivers
|
||||
|
||||
* `extras.signals.run_custom_validators()`
|
||||
|
||||
## core.job_start
|
||||
|
||||
This signal is sent whenever a [background job](../features/background-jobs.md) is started.
|
||||
|
||||
### Receivers
|
||||
|
||||
* `extras.signals.process_job_start_event_rules()`
|
||||
|
||||
## core.job_end
|
||||
|
||||
This signal is sent whenever a [background job](../features/background-jobs.md) is terminated.
|
||||
|
||||
### Receivers
|
||||
|
||||
* `extras.signals.process_job_end_event_rules()`
|
||||
|
||||
## core.pre_sync
|
||||
|
||||
This signal is sent when the [DataSource](../models/core/datasource.md) model's `sync()` method is called.
|
||||
|
||||
## core.post_sync
|
||||
|
||||
This signal is sent when a [DataSource](../models/core/datasource.md) finishes synchronizing.
|
||||
|
@ -26,9 +26,9 @@ To learn more about this feature, check out the [GraphQL API documentation](../i
|
||||
|
||||
## Webhooks
|
||||
|
||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are an excellent mechanism for building event-based automation processes.
|
||||
A webhook is a mechanism for conveying to some external system a change that has taken place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. To do this, first create a [webhook](../models/extras/webhook.md) identifying the remote receiver (URL), HTTP method, and any other necessary parameters. Then, define an [event rule](../models/extras/eventrule.md) which is triggered by device changes to transmit the webhook.
|
||||
|
||||
To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
|
||||
When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are an excellent mechanism for building event-based automation processes. To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
|
31
docs/features/event-rules.md
Normal file
31
docs/features/event-rules.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Event Rules
|
||||
|
||||
NetBox includes the ability to execute certain functions in response to internal object changes. These include:
|
||||
|
||||
* [Scripts](../customization/custom-scripts.md) execution
|
||||
* [Webhooks](../integrations/webhooks.md) execution
|
||||
|
||||
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
|
||||
|
||||
Each event must be associated with at least one NetBox object type and at least one event (e.g. create, update, or delete).
|
||||
|
||||
## Conditional Event Rules
|
||||
|
||||
An event rule may include a set of conditional logic expressed in JSON used to control whether an event triggers for a specific object. For example, you may wish to trigger an event for devices only when the `status` field of an object is "active":
|
||||
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
|
||||
|
||||
## Event Rule Processing
|
||||
|
||||
When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected in the admin UI under System > Background Tasks.
|
49
docs/features/vpn-tunnels.md
Normal file
49
docs/features/vpn-tunnels.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Tunnels
|
||||
|
||||
NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Termination1[TunnelTermination]
|
||||
Termination2[TunnelTermination]
|
||||
Interface1[Interface]
|
||||
Interface2[Interface]
|
||||
Tunnel --> Termination1 & Termination2
|
||||
Termination1 --> Interface1
|
||||
Termination2 --> Interface2
|
||||
Interface1 --> Device
|
||||
Interface2 --> VirtualMachine
|
||||
|
||||
click Tunnel "../../models/vpn/tunnel/"
|
||||
click TunnelTermination1 "../../models/vpn/tunneltermination/"
|
||||
click TunnelTermination2 "../../models/vpn/tunneltermination/"
|
||||
```
|
||||
|
||||
# IPSec & IKE
|
||||
|
||||
NetBox includes robust support for modeling IPSec & IKE policies. These are used to define encryption and authentication parameters for IPSec tunnels.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph IKEProposals[Proposals]
|
||||
IKEProposal1[IKEProposal]
|
||||
IKEProposal2[IKEProposal]
|
||||
end
|
||||
subgraph IPSecProposals[Proposals]
|
||||
IPSecProposal1[IPSecProposal]
|
||||
IPSecProposal2[IPSecProposal]
|
||||
end
|
||||
IKEProposals --> IKEPolicy
|
||||
IPSecProposals --> IPSecPolicy
|
||||
IKEPolicy & IPSecPolicy--> IPSecProfile
|
||||
IPSecProfile --> Tunnel
|
||||
|
||||
click IKEProposal1 "../../models/vpn/ikeproposal/"
|
||||
click IKEProposal2 "../../models/vpn/ikeproposal/"
|
||||
click IKEPolicy "../../models/vpn/ikepolicy/"
|
||||
click IPSecProposal1 "../../models/vpn/ipsecproposal/"
|
||||
click IPSecProposal2 "../../models/vpn/ipsecproposal/"
|
||||
click IPSecPolicy "../../models/vpn/ipsecpolicy/"
|
||||
click IPSecProfile "../../models/vpn/ipsecprofile/"
|
||||
click Tunnel "../../models/vpn/tunnel/"
|
||||
```
|
@ -32,7 +32,7 @@ In addition to its expansive and robust data model, NetBox offers myriad mechani
|
||||
* Custom fields
|
||||
* Custom model validation
|
||||
* Export templates
|
||||
* Webhooks
|
||||
* Event rules
|
||||
* Plugins
|
||||
* REST & GraphQL APIs
|
||||
|
||||
|
@ -227,6 +227,17 @@ sudo sh -c "echo 'boto3' >> /opt/netbox/local_requirements.txt"
|
||||
!!! info
|
||||
These packages were previously required in NetBox v3.5 but now are optional.
|
||||
|
||||
### Sentry Integration
|
||||
|
||||
NetBox may be configured to send error reports to [Sentry](../administration/error-reporting.md) for analysis. This integration requires installation of the `sentry-sdk` Python library.
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'sentry-sdk' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
!!! info
|
||||
Sentry integration was previously included by default in NetBox v3.6 but is now optional.
|
||||
|
||||
## Run the Upgrade Script
|
||||
|
||||
Once NetBox has been configured, we're ready to proceed with the actual installation. We'll run the packaged upgrade script (`upgrade.sh`) to perform the following actions:
|
||||
|
@ -1,11 +1,9 @@
|
||||
# Webhooks
|
||||
|
||||
NetBox can be configured to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks.
|
||||
NetBox can be configured via [Event Rules](../features/event-rules.md) to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks.
|
||||
|
||||
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. Webhooks will be sent automatically by NetBox whenever the configured constraints are met.
|
||||
|
||||
Each webhook must be associated with at least one NetBox object type and at least one event (create, update, or delete). Users can specify the receiver URL, HTTP request type (`GET`, `POST`, etc.), content type, and headers. A request body can also be specified; if left blank, this will default to a serialized representation of the affected object.
|
||||
|
||||
!!! warning "Security Notice"
|
||||
Webhooks support the inclusion of user-submitted code to generate the URL, custom headers, and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
|
||||
|
||||
@ -70,26 +68,12 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Webhooks
|
||||
|
||||
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
|
||||
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
|
||||
!!! note
|
||||
The setting of conditional webhooks has been moved to [Event Rules](../features/event-rules.md) since NetBox 3.7
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
|
||||
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
|
||||
|
||||
|
@ -19,10 +19,13 @@ NetBox was built specifically to serve the needs of network engineers and operat
|
||||
* Device modeling using pre-defined types
|
||||
* Virtual chassis and device contexts
|
||||
* Network, power, and console cabling with SVG traces
|
||||
* Breakout cables
|
||||
* Power distribution modeling
|
||||
* Data circuit and provider tracking
|
||||
* Wireless LAN and point-to-point links
|
||||
* L2 VPN overlays
|
||||
* VPN tunnels
|
||||
* IKE & IPSec policies
|
||||
* Layer 2 VPN overlays
|
||||
* FHRP groups (VRRP, HSRP, etc.)
|
||||
* Application service bindings
|
||||
* Virtual machines & clusters
|
||||
@ -30,13 +33,14 @@ NetBox was built specifically to serve the needs of network engineers and operat
|
||||
* Tenant ownership assignment
|
||||
* Device & VM configuration contexts for advanced configuration rendering
|
||||
* Custom fields for data model extension
|
||||
* Custom validation rules
|
||||
* Custom validation & protection rules
|
||||
* Custom reports & scripts executable directly within the UI
|
||||
* Extensive plugin framework for adding custom functionality
|
||||
* Single sign-on (SSO) authentication
|
||||
* Robust object-based permissions
|
||||
* Detailed, automatic change logging
|
||||
* Global search engine
|
||||
* Event-driven scripts & webhooks
|
||||
|
||||
## What NetBox Is Not
|
||||
|
||||
|
BIN
docs/media/misc/netbox_logo.png
Normal file
BIN
docs/media/misc/netbox_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
@ -77,6 +77,9 @@ If selected, this component will be treated as if a cable has been connected.
|
||||
|
||||
Virtual interfaces can be bound to a physical parent interface. This is helpful for modeling virtual interfaces which employ encapsulation on a physical interface, such as an 802.1Q VLAN-tagged subinterface.
|
||||
|
||||
!!! note
|
||||
An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.
|
||||
|
||||
### Bridged Interface
|
||||
|
||||
Interfaces can be bridged to other interfaces on a device in two manners: symmetric or grouped.
|
||||
|
@ -64,16 +64,25 @@ Defines how filters are evaluated against custom field values.
|
||||
| Loose | Match any occurrence of the value |
|
||||
| Exact | Match only the complete field value |
|
||||
|
||||
### UI Visibility
|
||||
### UI Visible
|
||||
|
||||
Controls how and whether the custom field is displayed within the NetBox user interface.
|
||||
Controls whether the custom field is displayed for objects within the NetBox user interface.
|
||||
|
||||
| Option | Description |
|
||||
|-------------------|--------------------------------------------------|
|
||||
| Read/write | Display and permit editing (default) |
|
||||
| Read-only | Display field but disallow editing |
|
||||
| Hidden | Do not display field in the UI |
|
||||
| Hidden (if unset) | Display in the UI only when a value has been set |
|
||||
| Option | Description |
|
||||
|--------|----------------------------------------------------------------|
|
||||
| Always | The field is always displayed when viewing an object (default) |
|
||||
| If set | The field is displayed only if a value has been defined |
|
||||
| Hidden | The field is not displayed when viewing an object |
|
||||
|
||||
### UI Editable
|
||||
|
||||
Controls whether the custom field is editable on objects within the NetBox user interface.
|
||||
|
||||
| Option | Description |
|
||||
|--------|------------------------------------------------------------------------------|
|
||||
| Yes | The field's value may be changed when editing an object (default) |
|
||||
| No | The field's value is displayed when editing an object but may not be altered |
|
||||
| Hidden | The field is not displayed when editing an object |
|
||||
|
||||
### Default
|
||||
|
||||
|
35
docs/models/extras/eventrule.md
Normal file
35
docs/models/extras/eventrule.md
Normal file
@ -0,0 +1,35 @@
|
||||
# EventRule
|
||||
|
||||
An event rule is a mechanism for automatically taking an action (such as running a script or sending a webhook) in response to an event in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating an event for device objects and designating a webhook to be transmitted. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver.
|
||||
|
||||
See the [event rules documentation](../../features/event-rules.md) for more information.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Content Types
|
||||
|
||||
The type(s) of object in NetBox that will trigger the rule.
|
||||
|
||||
### Enabled
|
||||
|
||||
If not selected, the event rule will not be processed.
|
||||
|
||||
### Events
|
||||
|
||||
The events which will trigger the rule. 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 |
|
||||
| Job starts | A job for an object starts |
|
||||
| Job ends | A job for an object terminates |
|
||||
|
||||
### 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, no action will be taken. An event rule that does not define any conditions will _always_ trigger.
|
@ -1,18 +0,0 @@
|
||||
# L2VPN Termination
|
||||
|
||||
A L2VPN termination is the attachment of an [L2VPN](./l2vpn.md) to an [interface](../dcim/interface.md) or [VLAN](./vlan.md). Note that the L2VPNs of the following types may have only two terminations assigned to them:
|
||||
|
||||
* VPWS
|
||||
* EPL
|
||||
* EP-LAN
|
||||
* EP-TREE
|
||||
|
||||
## Fields
|
||||
|
||||
### L2VPN
|
||||
|
||||
The [L2VPN](./l2vpn.md) instance.
|
||||
|
||||
### VLAN or Interface
|
||||
|
||||
The [VLAN](./vlan.md), [device interface](../dcim/interface.md), or [virtual machine interface](../virtualization/virtualmachine.md) attached to the L2VPN.
|
13
docs/models/virtualization/virtualdisk.md
Normal file
13
docs/models/virtualization/virtualdisk.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Virtual Disks
|
||||
|
||||
A virtual disk is used to model discrete virtual hard disks assigned to [virtual machines](./virtualmachine.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name that is unique to the assigned virtual machine.
|
||||
|
||||
### Size
|
||||
|
||||
The allocated disk size, in gigabytes.
|
@ -16,6 +16,9 @@ The interface's name. Must be unique to the assigned VM.
|
||||
|
||||
Identifies the parent interface of a subinterface (e.g. used to employ encapsulation).
|
||||
|
||||
!!! note
|
||||
An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.
|
||||
|
||||
### Bridged Interface
|
||||
|
||||
An interface on the same VM with which this interface is bridged.
|
||||
|
25
docs/models/vpn/ikepolicy.md
Normal file
25
docs/models/vpn/ikepolicy.md
Normal file
@ -0,0 +1,25 @@
|
||||
# IKE Policies
|
||||
|
||||
An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The unique user-assigned name for the policy.
|
||||
|
||||
### Version
|
||||
|
||||
The IKE version employed (v1 or v2).
|
||||
|
||||
### Mode
|
||||
|
||||
The IKE mode employed (main or aggressive).
|
||||
|
||||
### Proposals
|
||||
|
||||
One or more [IKE proposals](./ikeproposal.md) supported for use by this policy.
|
||||
|
||||
### Pre-shared Key
|
||||
|
||||
A pre-shared secret key associated with this policy (optional).
|
39
docs/models/vpn/ikeproposal.md
Normal file
39
docs/models/vpn/ikeproposal.md
Normal file
@ -0,0 +1,39 @@
|
||||
# IKE Proposals
|
||||
|
||||
An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) proposal defines a set of parameters used to establish a secure bidirectional connection across an untrusted medium, such as the Internet. IKE proposals defined in NetBox can be referenced by [IKE policies](./ikepolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
|
||||
|
||||
!!! note
|
||||
Some platforms refer to IKE proposals as [ISAKMP](https://en.wikipedia.org/wiki/Internet_Security_Association_and_Key_Management_Protocol), which is a framework for authentication and key exchange which employs IKE.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The unique user-assigned name for the proposal.
|
||||
|
||||
### Authentication Method
|
||||
|
||||
The strategy employed for authenticating the IKE peer. Available options are listed below.
|
||||
|
||||
| Name |
|
||||
|----------------|
|
||||
| Pre-shared key |
|
||||
| Certificate |
|
||||
| RSA signature |
|
||||
| DSA signature |
|
||||
|
||||
### Encryption Algorithm
|
||||
|
||||
The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations. Specifying an authentication algorithm is optional, as some encryption algorithms (e.g. AES-GCM) provide authentication natively.
|
||||
|
||||
### Group
|
||||
|
||||
The [Diffie-Hellman group](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) supported by the proposal. Group IDs are [managed by IANA](https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8).
|
||||
|
||||
### SA Lifetime
|
||||
|
||||
The maximum lifetime for the IKE security association (SA), in seconds.
|
17
docs/models/vpn/ipsecpolicy.md
Normal file
17
docs/models/vpn/ipsecpolicy.md
Normal file
@ -0,0 +1,17 @@
|
||||
# IPSec Policy
|
||||
|
||||
An [IPSec](https://en.wikipedia.org/wiki/IPsec) policy defines a set of [proposals](./ikeproposal.md) to be used in the formation of IPSec tunnels. A perfect forward secrecy (PFS) group may optionally also be defined. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The unique user-assigned name for the policy.
|
||||
|
||||
### Proposals
|
||||
|
||||
One or more [IPSec proposals](./ipsecproposal.md) supported for use by this policy.
|
||||
|
||||
### PFS Group
|
||||
|
||||
The [perfect forward secrecy (PFS)](https://en.wikipedia.org/wiki/Forward_secrecy) group supported by this policy (optional).
|
21
docs/models/vpn/ipsecprofile.md
Normal file
21
docs/models/vpn/ipsecprofile.md
Normal file
@ -0,0 +1,21 @@
|
||||
# IPSec Profile
|
||||
|
||||
An [IPSec](https://en.wikipedia.org/wiki/IPsec) profile defines an [IKE policy](./ikepolicy.md), [IPSec policy](./ipsecpolicy.md), and IPSec mode used for establishing an IPSec tunnel.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The unique user-assigned name for the profile.
|
||||
|
||||
### Mode
|
||||
|
||||
The IPSec mode employed by the profile: Encapsulating Security Payload (ESP) or Authentication Header (AH).
|
||||
|
||||
### IKE Policy
|
||||
|
||||
The [IKE policy](./ikepolicy.md) associated with the profile.
|
||||
|
||||
### IPSec Policy
|
||||
|
||||
The [IPSec policy](./ipsecpolicy.md) associated with the profile.
|
31
docs/models/vpn/ipsecproposal.md
Normal file
31
docs/models/vpn/ipsecproposal.md
Normal file
@ -0,0 +1,31 @@
|
||||
# IPSec Proposal
|
||||
|
||||
An [IPSec](https://en.wikipedia.org/wiki/IPsec) proposal defines a set of parameters used in negotiating security associations for IPSec tunnels. IPSec proposals defined in NetBox can be referenced by [IPSec policies](./ipsecpolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
The unique user-assigned name for the proposal.
|
||||
|
||||
### Encryption Algorithm
|
||||
|
||||
The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
|
||||
|
||||
!!! note
|
||||
If an encryption algorithm is not specified, an authentication algorithm must be specified.
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
|
||||
|
||||
!!! note
|
||||
If an authentication algorithm is not specified, an encryption algorithm must be specified.
|
||||
|
||||
### SA Lifetime (Seconds)
|
||||
|
||||
The maximum amount of time for which the security association (SA) may be active, in seconds.
|
||||
|
||||
### SA Lifetime (Data)
|
||||
|
||||
The maximum amount of data which can be transferred within the security association (SA) before it must be rebuilt, in kilobytes.
|
@ -1,6 +1,6 @@
|
||||
# L2VPN
|
||||
|
||||
A L2VPN object is NetBox is a representation of a layer 2 bridge technology such as VXLAN, VPLS, or EPL. Each L2VPN can be identified by name as well as by an optional unique identifier (VNI would be an example). Once created, L2VPNs can be terminated to [interfaces](../dcim/interface.md) and [VLANs](./vlan.md).
|
||||
A L2VPN object is NetBox is a representation of a layer 2 bridge technology such as VXLAN, VPLS, or EPL. Each L2VPN can be identified by name as well as by an optional unique identifier (VNI would be an example). Once created, L2VPNs can be terminated to [interfaces](../dcim/interface.md) and [VLANs](../ipam/vlan.md).
|
||||
|
||||
## Fields
|
||||
|
||||
@ -38,4 +38,4 @@ An optional numeric identifier. This can be used to track a pseudowire ID, for e
|
||||
|
||||
### Import & Export Targets
|
||||
|
||||
The [route targets](./routetarget.md) associated with this L2VPN to control the import and export of forwarding information.
|
||||
The [route targets](../ipam/routetarget.md) associated with this L2VPN to control the import and export of forwarding information.
|
18
docs/models/vpn/l2vpntermination.md
Normal file
18
docs/models/vpn/l2vpntermination.md
Normal file
@ -0,0 +1,18 @@
|
||||
# L2VPN Termination
|
||||
|
||||
A L2VPN termination is the attachment of an [L2VPN](./l2vpn.md) to an [interface](../dcim/interface.md) or [VLAN](../ipam/vlan.md). Note that the L2VPNs of the following types may have only two terminations assigned to them:
|
||||
|
||||
* VPWS
|
||||
* EPL
|
||||
* EP-LAN
|
||||
* EP-TREE
|
||||
|
||||
## Fields
|
||||
|
||||
### L2VPN
|
||||
|
||||
The [L2VPN](./l2vpn.md) instance.
|
||||
|
||||
### VLAN or Interface
|
||||
|
||||
The [VLAN](../ipam/vlan.md), [device interface](../dcim/interface.md), or [virtual machine interface](../virtualization/virtualmachine.md) attached to the L2VPN.
|
38
docs/models/vpn/tunnel.md
Normal file
38
docs/models/vpn/tunnel.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Tunnels
|
||||
|
||||
A tunnel represents a private virtual connection established among two or more endpoints across a shared infrastructure by employing protocol encapsulation. Common encapsulation techniques include [Generic Routing Encapsulation (GRE)](https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation), [IP-in-IP](https://en.wikipedia.org/wiki/IP_in_IP), and [IPSec](https://en.wikipedia.org/wiki/IPsec). NetBox supports modeling both peer-to-peer and hub-and-spoke tunnel topologies.
|
||||
|
||||
Device and virtual machine interfaces are associated to tunnels by creating [tunnel terminations](./tunneltermination.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique name assigned to the tunnel for identification.
|
||||
|
||||
### Status
|
||||
|
||||
The operational status of the tunnel. By default, the following statuses are available:
|
||||
|
||||
* Planned
|
||||
* Active
|
||||
* Disabled
|
||||
|
||||
!!! tip "Custom tunnel statuses"
|
||||
Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Group
|
||||
|
||||
The [administrative group](./tunnelgroup.md) to which this tunnel is assigned (optional).
|
||||
|
||||
### Encapsulation
|
||||
|
||||
The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.
|
||||
|
||||
### Tunnel ID
|
||||
|
||||
An optional numeric identifier for the tunnel.
|
||||
|
||||
### IPSec Profile
|
||||
|
||||
For IPSec tunnels, this is the [IPSec Profile](./ipsecprofile.md) employed to negotiate security associations.
|
13
docs/models/vpn/tunnelgroup.md
Normal file
13
docs/models/vpn/tunnelgroup.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Tunnel Group
|
||||
|
||||
[Tunnels](./tunnel.md) can be arranged into administrative groups for organization. For example, you might crete a group to manage all peer-to-peer tunnels inside a mesh network. The assignment of a tunnel to a group is optional.
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
30
docs/models/vpn/tunneltermination.md
Normal file
30
docs/models/vpn/tunneltermination.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Tunnel Terminations
|
||||
|
||||
A tunnel termination connects a device or virtual machine interface to a [tunnel](./tunnel.md). The tunnel must be created before any terminations may be added.
|
||||
|
||||
## Fields
|
||||
|
||||
### Tunnel
|
||||
|
||||
The [tunnel](./tunnel.md) to which this termination is made.
|
||||
|
||||
### Role
|
||||
|
||||
The functional role of the attached interface. The following options are available:
|
||||
|
||||
| Name | Description |
|
||||
|-------|--------------------------------------------------|
|
||||
| Peer | An endpoint in a point-to-point or mesh topology |
|
||||
| Hub | A central point in a hub-and-spoke topology |
|
||||
| Spoke | An edge point in a hub-and-spoke topology |
|
||||
|
||||
!!! note
|
||||
Multiple hub terminations may be attached to a tunnel.
|
||||
|
||||
### Termination
|
||||
|
||||
The device or virtual machine interface terminated to the tunnel.
|
||||
|
||||
### Outside IP
|
||||
|
||||
The public or underlay IP address with which this termination is associated. This is the IP to which peers will route tunneled traffic.
|
23
docs/plugins/development/data-backends.md
Normal file
23
docs/plugins/development/data-backends.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Data Backends
|
||||
|
||||
[Data sources](../../models/core/datasource.md) can be defined to reference data which exists on systems of record outside NetBox, such as a git repository or Amazon S3 bucket. Plugins can register their own backend classes to introduce support for additional resource types. This is done by subclassing NetBox's `DataBackend` class.
|
||||
|
||||
```python title="data_backends.py"
|
||||
from netbox.data_backends import DataBackend
|
||||
|
||||
class MyDataBackend(DataBackend):
|
||||
name = 'mybackend'
|
||||
label = 'My Backend'
|
||||
...
|
||||
```
|
||||
|
||||
To register one or more data backends with NetBox, define a list named `backends` at the end of this file:
|
||||
|
||||
```python title="data_backends.py"
|
||||
backends = [MyDataBackend]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
|
||||
|
||||
::: core.data_backends.DataBackend
|
@ -69,7 +69,7 @@ The plugin source directory contains all the actual Python code and other resour
|
||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||
|
||||
```python
|
||||
from extras.plugins import PluginConfig
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
class FooBarConfig(PluginConfig):
|
||||
name = 'foo_bar'
|
||||
@ -109,6 +109,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||
| `queues` | A list of custom background task queues to create |
|
||||
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
|
||||
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
|
||||
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
|
||||
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
|
||||
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
|
||||
@ -120,7 +121,7 @@ All required settings must be configured by the user. If a configuration paramet
|
||||
Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
|
||||
|
||||
```python
|
||||
from extras.plugins import get_plugin_config
|
||||
from netbox.plugins import get_plugin_config
|
||||
get_plugin_config('my_plugin', 'verbose_name')
|
||||
```
|
||||
|
||||
|
@ -60,6 +60,10 @@ class MyModel(NetBoxModel):
|
||||
|
||||
This attribute specifies the URL at which the documentation for this model can be reached. By default, it will return `/static/docs/models/<app_label>/<model_name>/`. Plugin models can override this to return a custom URL. For example, you might direct the user to your plugin's documentation hosted on [ReadTheDocs](https://readthedocs.org/).
|
||||
|
||||
#### `_netbox_private`
|
||||
|
||||
By default, any model introduced by a plugin will appear in the list of available object types e.g. when creating a custom field or certain dashboard widgets. If your model is intended only for "behind the scenes use" and should not be exposed to end users, set `_netbox_private` to True. This will omit it from the list of general-purpose object types.
|
||||
|
||||
### Enabling Features Individually
|
||||
|
||||
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)
|
||||
@ -119,14 +123,17 @@ For more information about database migrations, see the [Django documentation](h
|
||||
|
||||
::: netbox.models.features.CustomValidationMixin
|
||||
|
||||
::: netbox.models.features.EventRulesMixin
|
||||
|
||||
!!! note
|
||||
`EventRulesMixin` was renamed from `WebhooksMixin` in NetBox v3.7.
|
||||
|
||||
::: netbox.models.features.ExportTemplatesMixin
|
||||
|
||||
::: netbox.models.features.JournalingMixin
|
||||
|
||||
::: netbox.models.features.TagsMixin
|
||||
|
||||
::: netbox.models.features.WebhooksMixin
|
||||
|
||||
## Choice Sets
|
||||
|
||||
For model fields which support the selection of one or more values from a predefined list of choices, NetBox provides the `ChoiceSet` utility class. This can be used in place of a regular choices tuple to provide enhanced functionality, namely dynamic configuration and colorization. (See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/models/fields/#choices) on the `choices` parameter for supported model fields.)
|
||||
|
@ -5,7 +5,7 @@
|
||||
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"
|
||||
from extras.plugins import PluginMenu
|
||||
from netbox.plugins import PluginMenu
|
||||
|
||||
menu = PluginMenu(
|
||||
label='My Plugin',
|
||||
@ -49,7 +49,7 @@ menu_items = (item1, item2, item3)
|
||||
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
item1 = PluginMenuItem(
|
||||
|
@ -14,8 +14,11 @@ class MyModelIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('site', 'device', 'status', 'description')
|
||||
```
|
||||
|
||||
Fields listed in `display_attrs` will not be cached for search, but will be displayed alongside the object when it appears in global search results. This is helpful for conveying to the user additional information about an object.
|
||||
|
||||
To register one or more indexes with NetBox, define a list named `indexes` at the end of this file:
|
||||
|
||||
```python
|
||||
|
@ -87,3 +87,28 @@ The table column classes listed below are supported for use in plugins. These cl
|
||||
options:
|
||||
members:
|
||||
- __init__
|
||||
|
||||
## Extending Core Tables
|
||||
|
||||
!!! info "This feature was introduced in NetBox v3.7."
|
||||
|
||||
Plugins can register their own custom columns on core tables using the `register_table_column()` utility function. This allows a plugin to attach additional information, such as relationships to its own models, to built-in object lists.
|
||||
|
||||
```python
|
||||
import django_tables2
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.tables import SiteTable
|
||||
from utilities.tables import register_table_column
|
||||
|
||||
mycol = django_tables2.Column(
|
||||
verbose_name=_('My Column'),
|
||||
accessor=django_tables2.A('description')
|
||||
)
|
||||
|
||||
register_table_column(mycol, 'foo', SiteTable)
|
||||
```
|
||||
|
||||
You'll typically want to define an accessor identifying the desired model field or relationship when defining a custom column. See the [django-tables2 documentation](https://django-tables2.readthedocs.io/) for more information on creating custom columns.
|
||||
|
||||
::: utilities.tables.register_table_column
|
||||
|
@ -206,7 +206,7 @@ For example, accessing `{{ request.user }}` within a template will return the cu
|
||||
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
|
||||
|
||||
```python
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from netbox.plugins import PluginTemplateExtension
|
||||
from .models import Animal
|
||||
|
||||
class SiteAnimalCount(PluginTemplateExtension):
|
||||
|
@ -171,23 +171,23 @@ Some text to show that the reference links can follow later.
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: /static/netbox_logo.png "Logo Title Text 2"
|
||||
[logo]: /media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
```
|
||||
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: /static/netbox_logo.png "Logo Title Text 2"
|
||||
[logo]: ../media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
|
||||
<a name="code"></a>
|
||||
|
||||
|
@ -10,6 +10,17 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.7](./version-3.7.md) (December 2023)
|
||||
|
||||
* VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
* Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
|
||||
* Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
|
||||
* Object Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
|
||||
* Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
|
||||
* Improved Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
|
||||
* Table Column Registration for Plugins ([#14173](https://github.com/netbox-community/netbox/issues/14173))
|
||||
* Data Backend Registration for Plugins ([#13381](https://github.com/netbox-community/netbox/issues/13381))
|
||||
|
||||
#### [Version 3.6](./version-3.6.md) (August 2023)
|
||||
|
||||
* Relocated Admin UI Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
|
||||
|
138
docs/release-notes/version-3.7.md
Normal file
138
docs/release-notes/version-3.7.md
Normal file
@ -0,0 +1,138 @@
|
||||
# NetBox v3.7
|
||||
|
||||
## v3.7.0 (2023-12-29)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The following fields have been removed from the Webhook model: `content_types`, `type_create`, `type_update`, `type_delete`, `type_job_start`, `type_job_end`, `enabled`, and `conditions`. Webhooks are now tied to events via [event rules](../features/event-rules.md). New event rules will be created for any existing webhooks automatically upon upgrade.
|
||||
* The `ui_visibility` field on the [custom field model](../models/extras/customfield.md) has been replaced with two new fields: `ui_visible` and `ui_editable`. These new fields will have their values mapped from the original field automatically upon upgrade.
|
||||
* The `FeatureQuery` class used internally for querying content types by model feature has been removed. It has been replaced by the new `with_feature()` manager method on NetBox's proxy model for ContentType (`core.models.ContentType`).
|
||||
* The internal ConfigRevision model has moved from `extras` to `core`. Configuration history will be retained throughout the upgrade process.
|
||||
* The [L2VPN](../models/vpn/l2vpn.md) and [L2VPNTermination](../models/vpn/l2vpntermination.md) models have moved from the `ipam` app to the new `vpn` app. All object data will be retained, however please note that the relevant API endpoints have likewise moved to `/api/vpn/`.
|
||||
* The `CustomFieldsMixin`, `SavedFiltersMixin`, and `TagsMixin` classes have moved from the `extras.forms.mixins` module to `netbox.forms.mixins`.
|
||||
|
||||
### New Features
|
||||
|
||||
#### VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
|
||||
Several new models have been introduced to enable [VPN tunnel management](../features/vpn-tunnels.md). Users can now define tunnels with two or more terminations to represent peer-to-peer or hub-and-spoke topologies. Each termination is made to a virtual interface on a device or virtual machine. Additionally, users can define IKE and IPSec proposals and policies, which can be applied to tunnels to document encryption and authentication strategies.
|
||||
|
||||
#### Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
|
||||
|
||||
This release introduces [event rules](../features/event-rules.md), which can be used to send webhooks or execute custom scripts automatically in response to events that occur in NetBox. For example, it's now possible to run a custom script whenever a new site is created with a particular status or tag.
|
||||
|
||||
Event rules replace and extend functionality that was previously built into the webhook model. New event rules will be created for any existing webhooks automatically upon upgrade.
|
||||
|
||||
#### Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
|
||||
|
||||
A new [VirtualDisk](../models/virtualization/virtualdisk.md) model has been introduced to enable tracking the assignment of discrete virtual disks to virtual machines. The `size` field has been retained on the VirtualMachine model, and will be populated automatically with the aggregate size of all assigned virtual disks. (Users who opt to eschew the new model may continue using the VirtualMachine `size` attribute independently as in previous releases.)
|
||||
|
||||
#### Object Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
|
||||
|
||||
A new [`PROTECTION_RULES`](../configuration/data-validation.md#protection_rules) configuration parameter has been introduced. Similar to how [custom validation rules](../customization/custom-validation.md) can be used to enforce certain values for object attributes, protection rules guard against the deletion of objects which do not meet specified criteria. This enables an administrator to prevent, for example, the deletion of a site which has a status of "active."
|
||||
|
||||
#### Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
|
||||
|
||||
The `ui_visible` field on [the custom field model](../models/extras/customfield.md) has been superseded by two new fields, `ui_visible` and `ui_editable`, which control how and whether a custom field is displayed when view and editing an object, respectively. Separating these two functions into discrete fields allows more control over how each custom field is presented to users. The values of these fields will be appropriately set automatically during the upgrade process from the value of the original field.
|
||||
|
||||
#### Improved Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
|
||||
|
||||
Global search results now include additional context about each object, such as a description, status, and/or related objects. The set of attributes to be displayed is specific to each object type, and is defined by setting `display_attrs` under the object's [SearchIndex class](../plugins/development/search.md#netbox.search.SearchIndex).
|
||||
|
||||
#### Table Column Registration for Plugins ([#14173](https://github.com/netbox-community/netbox/issues/14173))
|
||||
|
||||
Plugins can now [register their own custom columns](../plugins/development/tables.md#extending-core-tables) for inclusion on core NetBox tables. For example, a plugin can register a new column on SiteTable using the new `register_table_column()` utility function, and it will become available for users to select for display.
|
||||
|
||||
#### Data Backend Registration for Plugins ([#13381](https://github.com/netbox-community/netbox/issues/13381))
|
||||
|
||||
Plugins can now [register their own data backends](../plugins/development/data-backends.md) for use with [synchronized data sources](../features/synchronized-data.md). This enables plugins to introduce new backends in addition to the git, S3, and local path backends provided natively.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#12135](https://github.com/netbox-community/netbox/issues/12135) - Avoid orphaned interfaces by preventing the deletion of interfaces which have children assigned
|
||||
* [#12216](https://github.com/netbox-community/netbox/issues/12216) - Add a `color` field for circuit types
|
||||
* [#13230](https://github.com/netbox-community/netbox/issues/13230) - Allow device types to be excluded from consideration when calculating a rack's utilization
|
||||
* [#13334](https://github.com/netbox-community/netbox/issues/13334) - Add an `error` field to the Job model to record any errors associated with its execution
|
||||
* [#13427](https://github.com/netbox-community/netbox/issues/13427) - Introduce a mechanism for excluding models from general-purpose lists of object types
|
||||
* [#13690](https://github.com/netbox-community/netbox/issues/13690) - Display any dependent objects to be deleted prior to deleting an object via the web UI
|
||||
* [#13794](https://github.com/netbox-community/netbox/issues/13794) - Any models with a relationship to Tenant are now included automatically in the list of related objects under the tenant view
|
||||
* [#13808](https://github.com/netbox-community/netbox/issues/13808) - Add a `/render-config` REST API endpoint for virtual machines
|
||||
* [#14035](https://github.com/netbox-community/netbox/issues/14035) - Order objects of equivalent weight by value in global search results to improve readability
|
||||
* [#14147](https://github.com/netbox-community/netbox/issues/14147) - Avoid recording empty changelog entries via the new `CHANGELOG_SKIP_EMPTY_CHANGES` config parameter
|
||||
* [#14156](https://github.com/netbox-community/netbox/issues/14156) - Enable custom fields for contact assignments
|
||||
* [#14240](https://github.com/netbox-community/netbox/issues/14240) - Increase maximum values for custom field minimum & maximum numeric validators
|
||||
* [#14361](https://github.com/netbox-community/netbox/issues/14361) - Add a `description` field for webhooks
|
||||
* [#14365](https://github.com/netbox-community/netbox/issues/14365) - Introduce `job_start` and `job_end` signals to allow automated plugin actions
|
||||
* [#14434](https://github.com/netbox-community/netbox/issues/14434) - Add model-specific termination object filters for cables (e.g. `interface_id` and `consoleport_id`)
|
||||
* [#14436](https://github.com/netbox-community/netbox/issues/14436) - Add PostgreSQL indexes for all GenericForeignKey fields
|
||||
* [#14579](https://github.com/netbox-community/netbox/issues/14579) - Allow users to specify a preferred language for UI translations
|
||||
|
||||
### Translations
|
||||
|
||||
* [#14075](https://github.com/netbox-community/netbox/issues/14075) - Add Spanish translation
|
||||
* [#14096](https://github.com/netbox-community/netbox/issues/14096) - Add French translation
|
||||
* [#14145](https://github.com/netbox-community/netbox/issues/14145) - Add Portuguese translation
|
||||
* [#14266](https://github.com/netbox-community/netbox/issues/14266) - Add Russian translation
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Fix hyperlinks for global search result attributes
|
||||
* [#14472](https://github.com/netbox-community/netbox/issues/14472) - Fix display of hidden custom fields in object edit forms
|
||||
* [#14499](https://github.com/netbox-community/netbox/issues/14499) - Relax requirements for encryption/auth algorithms on IKE & IPSec proposals
|
||||
* [#14550](https://github.com/netbox-community/netbox/issues/14550) - Fix changing action type of existing event rule
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#13550](https://github.com/netbox-community/netbox/issues/13550) - Optimize the format for declaring view actions under `ActionsMixin` (backward compatibility has been retained)
|
||||
* [#13645](https://github.com/netbox-community/netbox/issues/13645) - Installation of the `sentry-sdk` Python library is now required only if Sentry reporting is enabled
|
||||
* [#14036](https://github.com/netbox-community/netbox/issues/14036) - Move plugin resources from the `extras` app into `netbox` (backward compatibility has been retained)
|
||||
* [#14153](https://github.com/netbox-community/netbox/issues/14153) - Replace `FeatureQuery` with new `with_feature()` method on proxy ContentType manager
|
||||
* [#14311](https://github.com/netbox-community/netbox/issues/14311) - Move the L2VPN models from the `ipam` app to the new `vpn` app
|
||||
* [#14312](https://github.com/netbox-community/netbox/issues/14312) - Move the ConfigRevision model from the `extras` app to `core`
|
||||
* [#14326](https://github.com/netbox-community/netbox/issues/14326) - Form feature mixin classes have been moved from the `extras` app to `netbox`
|
||||
* [#14395](https://github.com/netbox-community/netbox/issues/14395) - Move `extras.webhooks_worker.process_webhook()` to `extras.webhooks.send_webhook()` (backward compatibility has been retained)
|
||||
* [#14424](https://github.com/netbox-community/netbox/issues/14424) - Remove change logging functionality from StagedChange
|
||||
* [#14458](https://github.com/netbox-community/netbox/issues/14458) - Remove the obsolete `clearcache` management command
|
||||
* [#14536](https://github.com/netbox-community/netbox/issues/14536) - Enforce uniqueness by default for non-VRF prefixes & IP addresses (`ENFORCE_GLOBAL_UNIQUE` now defaults to true)
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Introduced the following endpoints:
|
||||
* `/api/extras/event-rules/`
|
||||
* `/api/virtualization/virtual-disks/`
|
||||
* `/api/vpn/ike-policies/`
|
||||
* `/api/vpn/ike-proposals/`
|
||||
* `/api/vpn/ipsec-policies/`
|
||||
* `/api/vpn/ipsec-profiles/`
|
||||
* `/api/vpn/ipsec-proposals/`
|
||||
* `/api/vpn/tunnels/`
|
||||
* `/api/vpn/tunnel-terminations/`
|
||||
* The following endpoints have been moved:
|
||||
* `/api/ipam/l2vpns/` -> `/api/vpn/l2vpns/`
|
||||
* `/api/ipam/l2vpn-terminations/` -> `/api/vpn/l2vpn-terminations/`
|
||||
* circuits.CircuitType
|
||||
* Added the optional `color` choice field
|
||||
* core.Job
|
||||
* Added the read-only `error` character field
|
||||
* extras.Webhook
|
||||
* Removed the following fields (these have been moved to the new `EventRule` model):
|
||||
* `content_types`
|
||||
* `type_create`
|
||||
* `type_update`
|
||||
* `type_delete`
|
||||
* `type_job_start`
|
||||
* `type_job_end`
|
||||
* `enabled`
|
||||
* `conditions`
|
||||
* Add the optional `description` field
|
||||
* dcim.DeviceType
|
||||
* Added the `exclude_from_utilization` boolean field
|
||||
* extras.CustomField
|
||||
* Removed the `ui_visibility` field
|
||||
* Added the `ui_visible` and `ui_editable` choice fields
|
||||
* tenancy.ContactAssignment
|
||||
* Added support for custom fields
|
||||
* virtualization.VirtualDisk
|
||||
* Added the read-only `virtual_disk_count` integer field
|
||||
* virtualization.VirtualMachine
|
||||
* Added the `/render-config` endpoint
|
23
mkdocs.yml
23
mkdocs.yml
@ -53,8 +53,8 @@ markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
@ -74,6 +74,7 @@ nav:
|
||||
- Circuits: 'features/circuits.md'
|
||||
- Wireless: 'features/wireless.md'
|
||||
- Virtualization: 'features/virtualization.md'
|
||||
- VPN Tunnels: 'features/vpn-tunnels.md'
|
||||
- Tenancy: 'features/tenancy.md'
|
||||
- Contacts: 'features/contacts.md'
|
||||
- Search: 'features/search.md'
|
||||
@ -82,6 +83,7 @@ nav:
|
||||
- Synchronized Data: 'features/synchronized-data.md'
|
||||
- Change Logging: 'features/change-logging.md'
|
||||
- Journaling: 'features/journaling.md'
|
||||
- Event Rules: 'features/event-rules.md'
|
||||
- Background Jobs: 'features/background-jobs.md'
|
||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||
- API & Integration: 'features/api-integration.md'
|
||||
@ -136,6 +138,7 @@ nav:
|
||||
- Forms: 'plugins/development/forms.md'
|
||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||
- Search: 'plugins/development/search.md'
|
||||
- Data Backends: 'plugins/development/data-backends.md'
|
||||
- REST API: 'plugins/development/rest-api.md'
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
@ -213,6 +216,7 @@ nav:
|
||||
- CustomField: 'models/extras/customfield.md'
|
||||
- CustomFieldChoiceSet: 'models/extras/customfieldchoiceset.md'
|
||||
- CustomLink: 'models/extras/customlink.md'
|
||||
- EventRule: 'models/extras/eventrule.md'
|
||||
- ExportTemplate: 'models/extras/exporttemplate.md'
|
||||
- ImageAttachment: 'models/extras/imageattachment.md'
|
||||
- JournalEntry: 'models/extras/journalentry.md'
|
||||
@ -228,8 +232,6 @@ nav:
|
||||
- FHRPGroupAssignment: 'models/ipam/fhrpgroupassignment.md'
|
||||
- IPAddress: 'models/ipam/ipaddress.md'
|
||||
- IPRange: 'models/ipam/iprange.md'
|
||||
- L2VPN: 'models/ipam/l2vpn.md'
|
||||
- L2VPNTermination: 'models/ipam/l2vpntermination.md'
|
||||
- Prefix: 'models/ipam/prefix.md'
|
||||
- RIR: 'models/ipam/rir.md'
|
||||
- Role: 'models/ipam/role.md'
|
||||
@ -250,7 +252,19 @@ nav:
|
||||
- ClusterGroup: 'models/virtualization/clustergroup.md'
|
||||
- ClusterType: 'models/virtualization/clustertype.md'
|
||||
- VMInterface: 'models/virtualization/vminterface.md'
|
||||
- VirtualDisk: 'models/virtualization/virtualdisk.md'
|
||||
- VirtualMachine: 'models/virtualization/virtualmachine.md'
|
||||
- VPN:
|
||||
- IKEPolicy: 'models/vpn/ikepolicy.md'
|
||||
- IKEProposal: 'models/vpn/ikeproposal.md'
|
||||
- IPSecPolicy: 'models/vpn/ipsecpolicy.md'
|
||||
- IPSecProfile: 'models/vpn/ipsecprofile.md'
|
||||
- IPSecProposal: 'models/vpn/ipsecproposal.md'
|
||||
- L2VPN: 'models/vpn/l2vpn.md'
|
||||
- L2VPNTermination: 'models/vpn/l2vpntermination.md'
|
||||
- Tunnel: 'models/vpn/tunnel.md'
|
||||
- TunnelGroup: 'models/vpn/tunnelgroup.md'
|
||||
- TunnelTermination: 'models/vpn/tunneltermination.md'
|
||||
- Wireless:
|
||||
- WirelessLAN: 'models/wireless/wirelesslan.md'
|
||||
- WirelessLANGroup: 'models/wireless/wirelesslangroup.md'
|
||||
@ -276,6 +290,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 3.7: 'release-notes/version-3.7.md'
|
||||
- Version 3.6: 'release-notes/version-3.6.md'
|
||||
- Version 3.5: 'release-notes/version-3.5.md'
|
||||
- Version 3.4: 'release-notes/version-3.4.md'
|
||||
|
@ -7,6 +7,8 @@ class UserToken(Token):
|
||||
"""
|
||||
Proxy model for users to manage their own API tokens.
|
||||
"""
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = 'token'
|
||||
|
@ -13,6 +13,7 @@ from django.shortcuts import render, resolve_url
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
@ -193,8 +194,16 @@ class UserConfigView(LoginRequiredMixin, View):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
messages.success(request, "Your preferences have been updated.")
|
||||
return redirect('account:preferences')
|
||||
messages.success(request, _("Your preferences have been updated."))
|
||||
response = redirect('account:preferences')
|
||||
|
||||
# Set/clear language cookie
|
||||
if language := form.cleaned_data['locale.language']:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||
else:
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
return response
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
|
@ -85,7 +85,7 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
]
|
||||
|
||||
|
@ -139,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
|
||||
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
@ -156,12 +156,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('ProviderAccount (ID)'),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitType.objects.all(),
|
||||
|
@ -7,7 +7,7 @@ from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
@ -91,6 +91,10 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
@ -99,9 +103,9 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
(None, ('description',)),
|
||||
(None, ('color', 'description')),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
@ -3,6 +3,7 @@ from django import forms
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
@ -64,7 +65,10 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'description', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
help_texts = {
|
||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitImportForm(NetBoxModelImportForm):
|
||||
|
@ -7,7 +7,7 @@ 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.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
@ -88,7 +88,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Provider')
|
||||
)
|
||||
service_id = forms.CharField(
|
||||
label=_('Service id'),
|
||||
label=_('Service ID'),
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
@ -97,8 +97,17 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Attributes'), ('color',)),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Circuit
|
||||
|
@ -76,14 +76,14 @@ class CircuitTypeForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
(_('Circuit Type'), (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
18
netbox/circuits/migrations/0043_circuittype_color.py
Normal file
18
netbox/circuits/migrations/0043_circuittype_color.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-20 21:25
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0042_provideraccount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(blank=True, max_length=6),
|
||||
),
|
||||
]
|
@ -7,6 +7,7 @@ from circuits.choices import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@ -20,6 +21,11 @@ class CircuitType(OrganizationalModel):
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
|
@ -10,6 +10,7 @@ class CircuitIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@ -22,6 +23,7 @@ class CircuitTerminationIndex(SearchIndex):
|
||||
('port_speed', 2000),
|
||||
('upstream_speed', 2000),
|
||||
)
|
||||
display_attrs = ('circuit', 'site', 'provider_network', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@ -32,6 +34,7 @@ class CircuitTypeIndex(SearchIndex):
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
@ -42,6 +45,7 @@ class ProviderIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
class ProviderAccountIndex(SearchIndex):
|
||||
@ -51,6 +55,7 @@ class ProviderAccountIndex(SearchIndex):
|
||||
('account', 200),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'account', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@ -62,3 +67,4 @@ class ProviderNetworkIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'service_id', 'description')
|
||||
|
@ -28,6 +28,7 @@ class CircuitTypeTable(NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuittype_list'
|
||||
)
|
||||
@ -40,7 +41,7 @@ class CircuitTypeTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||
|
||||
|
@ -4,6 +4,7 @@ from core.choices import *
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
@ -19,7 +20,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=DataSourceTypeChoices
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=DataSourceStatusChoices,
|
||||
@ -68,5 +69,5 @@ class JobSerializer(BaseModelSerializer):
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'job_id',
|
||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
]
|
||||
|
@ -7,18 +7,6 @@ from utilities.choices import ChoiceSet
|
||||
# Data sources
|
||||
#
|
||||
|
||||
class DataSourceTypeChoices(ChoiceSet):
|
||||
LOCAL = 'local'
|
||||
GIT = 'git'
|
||||
AMAZON_S3 = 'amazon-s3'
|
||||
|
||||
CHOICES = (
|
||||
(LOCAL, _('Local'), 'gray'),
|
||||
(GIT, 'Git', 'blue'),
|
||||
(AMAZON_S3, 'Amazon S3', 'blue'),
|
||||
)
|
||||
|
||||
|
||||
class DataSourceStatusChoices(ChoiceSet):
|
||||
NEW = 'new'
|
||||
QUEUED = 'queued'
|
||||
|
@ -10,61 +10,24 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.registry import registry
|
||||
from .choices import DataSourceTypeChoices
|
||||
from netbox.data_backends import DataBackend
|
||||
from netbox.utils import register_data_backend
|
||||
from .exceptions import SyncError
|
||||
|
||||
__all__ = (
|
||||
'LocalBackend',
|
||||
'GitBackend',
|
||||
'LocalBackend',
|
||||
'S3Backend',
|
||||
)
|
||||
|
||||
logger = logging.getLogger('netbox.data_backends')
|
||||
|
||||
|
||||
def register_backend(name):
|
||||
"""
|
||||
Decorator for registering a DataBackend class.
|
||||
"""
|
||||
|
||||
def _wrapper(cls):
|
||||
registry['data_backends'][name] = cls
|
||||
return cls
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
class DataBackend:
|
||||
parameters = {}
|
||||
sensitive_parameters = []
|
||||
|
||||
# Prevent Django's template engine from calling the backend
|
||||
# class when referenced via DataSource.backend_class
|
||||
do_not_call_in_templates = True
|
||||
|
||||
def __init__(self, url, **kwargs):
|
||||
self.url = url
|
||||
self.params = kwargs
|
||||
self.config = self.init_config()
|
||||
|
||||
def init_config(self):
|
||||
"""
|
||||
Hook to initialize the instance's configuration.
|
||||
"""
|
||||
return
|
||||
|
||||
@property
|
||||
def url_scheme(self):
|
||||
return urlparse(self.url).scheme.lower()
|
||||
|
||||
@contextmanager
|
||||
def fetch(self):
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
@register_backend(DataSourceTypeChoices.LOCAL)
|
||||
@register_data_backend()
|
||||
class LocalBackend(DataBackend):
|
||||
name = 'local'
|
||||
label = _('Local')
|
||||
is_local = True
|
||||
|
||||
@contextmanager
|
||||
def fetch(self):
|
||||
@ -74,8 +37,10 @@ class LocalBackend(DataBackend):
|
||||
yield local_path
|
||||
|
||||
|
||||
@register_backend(DataSourceTypeChoices.GIT)
|
||||
@register_data_backend()
|
||||
class GitBackend(DataBackend):
|
||||
name = 'git'
|
||||
label = 'Git'
|
||||
parameters = {
|
||||
'username': forms.CharField(
|
||||
required=False,
|
||||
@ -144,8 +109,10 @@ class GitBackend(DataBackend):
|
||||
local_path.cleanup()
|
||||
|
||||
|
||||
@register_backend(DataSourceTypeChoices.AMAZON_S3)
|
||||
@register_data_backend()
|
||||
class S3Backend(DataBackend):
|
||||
name = 'amazon-s3'
|
||||
label = 'Amazon S3'
|
||||
parameters = {
|
||||
'aws_access_key_id': forms.CharField(
|
||||
label=_('AWS access key ID'),
|
||||
|
@ -4,10 +4,12 @@ from django.utils.translation import gettext as _
|
||||
import django_filters
|
||||
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevisionFilterSet',
|
||||
'DataFileFilterSet',
|
||||
'DataSourceFilterSet',
|
||||
'JobFilterSet',
|
||||
@ -16,7 +18,7 @@ __all__ = (
|
||||
|
||||
class DataSourceFilterSet(NetBoxModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=DataSourceTypeChoices,
|
||||
choices=get_data_backend_choices,
|
||||
null_value=None
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
@ -122,3 +124,23 @@ class JobFilterSet(BaseFilterSet):
|
||||
Q(user__username__icontains=value) |
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigRevision
|
||||
fields = [
|
||||
'id',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(comment__icontains=value)
|
||||
)
|
||||
|
@ -1,10 +1,9 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.choices import DataSourceTypeChoices
|
||||
from core.models import *
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms.fields import CommentField
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||
|
||||
@ -16,9 +15,8 @@ __all__ = (
|
||||
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
type = forms.ChoiceField(
|
||||
label=_('Type'),
|
||||
choices=add_blank_choice(DataSourceTypeChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
choices=get_data_backend_choices,
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
|
@ -1,18 +1,18 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.choices import *
|
||||
from core.models import *
|
||||
from extras.forms.mixins import SavedFiltersMixin
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevisionFilterForm',
|
||||
'DataFileFilterForm',
|
||||
'DataSourceFilterForm',
|
||||
'JobFilterForm',
|
||||
@ -27,7 +27,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
choices=DataSourceTypeChoices,
|
||||
choices=get_data_backend_choices,
|
||||
required=False
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object Type'),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('jobs').get_query()),
|
||||
queryset=ContentType.objects.with_feature('jobs'),
|
||||
required=False,
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
@ -124,3 +124,9 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
)
|
||||
|
@ -1,23 +1,34 @@
|
||||
import copy
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from core.models import *
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from netbox.registry import registry
|
||||
from utilities.forms import get_field_value
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BootstrapMixin, get_field_value
|
||||
from utilities.forms.fields import CommentField
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevisionForm',
|
||||
'DataSourceForm',
|
||||
'ManagedFileForm',
|
||||
)
|
||||
|
||||
EMPTY_VALUES = ('', None, [], ())
|
||||
|
||||
|
||||
class DataSourceForm(NetBoxModelForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=get_data_backend_choices,
|
||||
widget=HTMXSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
@ -26,7 +37,6 @@ class DataSourceForm(NetBoxModelForm):
|
||||
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'type': HTMXSelect(),
|
||||
'ignore_rules': forms.Textarea(
|
||||
attrs={
|
||||
'rows': 5,
|
||||
@ -56,12 +66,13 @@ class DataSourceForm(NetBoxModelForm):
|
||||
|
||||
# Add backend-specific form fields
|
||||
self.backend_fields = []
|
||||
for name, form_field in backend.parameters.items():
|
||||
field_name = f'backend_{name}'
|
||||
self.backend_fields.append(field_name)
|
||||
self.fields[field_name] = copy.copy(form_field)
|
||||
if self.instance and self.instance.parameters:
|
||||
self.fields[field_name].initial = self.instance.parameters.get(name)
|
||||
if backend:
|
||||
for name, form_field in backend.parameters.items():
|
||||
field_name = f'backend_{name}'
|
||||
self.backend_fields.append(field_name)
|
||||
self.fields[field_name] = copy.copy(form_field)
|
||||
if self.instance and self.instance.parameters:
|
||||
self.fields[field_name].initial = self.instance.parameters.get(name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@ -106,3 +117,113 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
|
||||
new_file.write(self.cleaned_data['upload_file'].read())
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
|
||||
# Emulate a declared field for each supported configuration parameter
|
||||
param_fields = {}
|
||||
for param in PARAMS:
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
'label': param.label,
|
||||
'help_text': param.description,
|
||||
}
|
||||
field_kwargs.update(**param.field_kwargs)
|
||||
param_fields[param.name] = param.field(**field_kwargs)
|
||||
attrs.update(param_fields)
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
|
||||
"""
|
||||
Form for creating a new ConfigRevision.
|
||||
"""
|
||||
|
||||
fieldsets = (
|
||||
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
|
||||
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
|
||||
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
|
||||
(_('Security'), ('ALLOWED_URL_SCHEMES',)),
|
||||
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
|
||||
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
|
||||
(_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
|
||||
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
|
||||
(_('Miscellaneous'), (
|
||||
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
|
||||
)),
|
||||
(_('Config Revision'), ('comment',))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigRevision
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'comment': forms.Textarea(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Append current parameter values to form field help texts and check for static configurations
|
||||
config = get_config()
|
||||
for param in PARAMS:
|
||||
value = getattr(config, param.name)
|
||||
|
||||
# Set the field's initial value, if it can be serialized. (This may not be the case e.g. for
|
||||
# CUSTOM_VALIDATORS, which may reference Python objects.)
|
||||
try:
|
||||
json.dumps(value)
|
||||
if type(value) in (tuple, list):
|
||||
self.fields[param.name].initial = ', '.join(value)
|
||||
else:
|
||||
self.fields[param.name].initial = value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Check whether this parameter is statically configured (e.g. in configuration.py)
|
||||
if hasattr(settings, param.name):
|
||||
self.fields[param.name].disabled = True
|
||||
self.fields[param.name].help_text = _(
|
||||
'This parameter has been defined statically and cannot be modified.'
|
||||
)
|
||||
continue
|
||||
|
||||
# Set the field's help text
|
||||
help_text = self.fields[param.name].help_text
|
||||
if help_text:
|
||||
help_text += '<br />' # Line break
|
||||
help_text += _('Current value: <strong>{value}</strong>').format(value=value or '—')
|
||||
if value == param.default:
|
||||
help_text += _(' (default)')
|
||||
self.fields[param.name].help_text = help_text
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
|
||||
# Populate JSON data on the instance
|
||||
instance.data = self.render_json()
|
||||
|
||||
if commit:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
def render_json(self):
|
||||
json = {}
|
||||
|
||||
# Iterate through each field and populate non-empty values
|
||||
for field_name in self.declared_fields:
|
||||
if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
|
||||
json[field_name] = self.cleaned_data[field_name]
|
||||
|
||||
return json
|
||||
|
@ -25,7 +25,7 @@ def sync_datasource(job, *args, **kwargs):
|
||||
job.terminate()
|
||||
|
||||
except Exception as e:
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED)
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
if type(e) in (SyncError, JobTimeoutException):
|
||||
logging.error(e)
|
||||
|
@ -1,20 +0,0 @@
|
||||
from django.core.cache import cache
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from extras.models import ConfigRevision
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Command to clear the entire cache."""
|
||||
help = 'Clears the cache.'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Fetch the current config revision from the cache
|
||||
config_version = cache.get('config_version')
|
||||
# Clear the cache
|
||||
cache.clear()
|
||||
self.stdout.write('Cache has been cleared.', ending="\n")
|
||||
if config_version:
|
||||
# Activate the current config revision
|
||||
ConfigRevision.objects.get(id=config_version).activate()
|
||||
self.stdout.write(f'Config revision ({config_version}) has been restored.', ending="\n")
|
@ -6,10 +6,11 @@ from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
|
||||
from core.models import ContentType
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
|
@ -4,7 +4,6 @@ from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -30,7 +29,7 @@ class Migration(migrations.Migration):
|
||||
('status', models.CharField(default='pending', max_length=30)),
|
||||
('data', models.JSONField(blank=True, null=True)),
|
||||
('job_id', models.UUIDField(unique=True)),
|
||||
('object_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('jobs'), on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
|
||||
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-20 17:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_job_created_auto_now'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='datasource',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
]
|
18
netbox/core/migrations/0007_job_add_error_field.py
Normal file
18
netbox/core/migrations/0007_job_add_error_field.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-23 20:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_datasource_type_remove_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='error',
|
||||
field=models.TextField(blank=True, editable=False),
|
||||
),
|
||||
]
|
29
netbox/core/migrations/0008_contenttype_proxy.py
Normal file
29
netbox/core/migrations/0008_contenttype_proxy.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-31 19:38
|
||||
|
||||
import core.models.contenttypes
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0007_job_add_error_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentType',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('contenttypes.contenttype',),
|
||||
managers=[
|
||||
('objects', core.models.contenttypes.ContentTypeManager()),
|
||||
],
|
||||
),
|
||||
]
|
31
netbox/core/migrations/0009_configrevision.py
Normal file
31
netbox/core/migrations/0009_configrevision.py
Normal file
@ -0,0 +1,31 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_contenttype_proxy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='ConfigRevision',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.CharField(blank=True, max_length=200)),
|
||||
('data', models.JSONField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'config revision',
|
||||
'verbose_name_plural': 'config revisions',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
],
|
||||
# Table will be renamed from extras_configrevision in extras/0101_move_configrevision
|
||||
database_operations=[],
|
||||
),
|
||||
]
|
17
netbox/core/migrations/0010_gfk_indexes.py
Normal file
17
netbox/core/migrations/0010_gfk_indexes.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-07 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_configrevision'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='job',
|
||||
index=models.Index(fields=['object_type', 'object_id'], name='core_job_object__c664ac_idx'),
|
||||
),
|
||||
]
|
@ -1,3 +1,5 @@
|
||||
from .config import *
|
||||
from .contenttypes import *
|
||||
from .data import *
|
||||
from .files import *
|
||||
from .jobs import *
|
||||
|
66
netbox/core/models/config.py
Normal file
66
netbox/core/models/config.py
Normal file
@ -0,0 +1,66 @@
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevision',
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevision(models.Model):
|
||||
"""
|
||||
An atomic revision of NetBox's configuration.
|
||||
"""
|
||||
created = models.DateTimeField(
|
||||
verbose_name=_('created'),
|
||||
auto_now_add=True
|
||||
)
|
||||
comment = models.CharField(
|
||||
verbose_name=_('comment'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
data = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('configuration data')
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
verbose_name = _('config revision')
|
||||
verbose_name_plural = _('config revisions')
|
||||
|
||||
def __str__(self):
|
||||
if not self.pk:
|
||||
return gettext('Default configuration')
|
||||
if self.is_active:
|
||||
return gettext('Current configuration')
|
||||
return gettext('Config revision #{id}').format(id=self.pk)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.data:
|
||||
return self.data[item]
|
||||
return super().__getattribute__(item)
|
||||
|
||||
def get_absolute_url(self):
|
||||
if not self.pk:
|
||||
return reverse('core:config') # Default config view
|
||||
return reverse('core:configrevision', args=[self.pk])
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
Cache the configuration data.
|
||||
"""
|
||||
cache.set('config', self.data, None)
|
||||
cache.set('config_version', self.pk, None)
|
||||
activate.alters_data = True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return cache.get('config_version') == self.pk
|
50
netbox/core/models/contenttypes.py
Normal file
50
netbox/core/models/contenttypes.py
Normal file
@ -0,0 +1,50 @@
|
||||
from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_
|
||||
from django.db.models import Q
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
__all__ = (
|
||||
'ContentType',
|
||||
'ContentTypeManager',
|
||||
)
|
||||
|
||||
|
||||
class ContentTypeManager(ContentTypeManager_):
|
||||
|
||||
def public(self):
|
||||
"""
|
||||
Filter the base queryset to return only ContentTypes corresponding to "public" models; those which are listed
|
||||
in registry['models'] and intended for reference by other objects.
|
||||
"""
|
||||
q = Q()
|
||||
for app_label, models in registry['models'].items():
|
||||
q |= Q(app_label=app_label, model__in=models)
|
||||
return self.get_queryset().filter(q)
|
||||
|
||||
def with_feature(self, feature):
|
||||
"""
|
||||
Return the ContentTypes only for models which are registered as supporting the specified feature. For example,
|
||||
we can find all ContentTypes for models which support webhooks with
|
||||
|
||||
ContentType.objects.with_feature('event_rules')
|
||||
"""
|
||||
if feature not in registry['model_features']:
|
||||
raise KeyError(
|
||||
f"{feature} is not a registered model feature! Valid features are: {registry['model_features'].keys()}"
|
||||
)
|
||||
|
||||
q = Q()
|
||||
for app_label, models in registry['model_features'][feature].items():
|
||||
q |= Q(app_label=app_label, model__in=models)
|
||||
|
||||
return self.get_queryset().filter(q)
|
||||
|
||||
|
||||
class ContentType(ContentType_):
|
||||
"""
|
||||
Wrap Django's native ContentType model to use our custom manager.
|
||||
"""
|
||||
objects = ContentTypeManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
@ -45,9 +44,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_('type'),
|
||||
max_length=50,
|
||||
choices=DataSourceTypeChoices,
|
||||
default=DataSourceTypeChoices.LOCAL
|
||||
max_length=50
|
||||
)
|
||||
source_url = models.CharField(
|
||||
max_length=200,
|
||||
@ -96,8 +93,9 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def docs_url(self):
|
||||
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
|
||||
|
||||
def get_type_color(self):
|
||||
return DataSourceTypeChoices.colors.get(self.type)
|
||||
def get_type_display(self):
|
||||
if backend := registry['data_backends'].get(self.type):
|
||||
return backend.label
|
||||
|
||||
def get_status_color(self):
|
||||
return DataSourceStatusChoices.colors.get(self.status)
|
||||
@ -110,10 +108,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def backend_class(self):
|
||||
return registry['data_backends'].get(self.type)
|
||||
|
||||
@property
|
||||
def is_local(self):
|
||||
return self.type == DataSourceTypeChoices.LOCAL
|
||||
|
||||
@property
|
||||
def ready_for_sync(self):
|
||||
return self.enabled and self.status not in (
|
||||
@ -124,8 +118,14 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate data backend type
|
||||
if self.type and self.type not in registry['data_backends']:
|
||||
raise ValidationError({
|
||||
'type': _("Unknown backend type: {type}".format(type=self.type))
|
||||
})
|
||||
|
||||
# Ensure URL scheme matches selected type
|
||||
if self.type == DataSourceTypeChoices.LOCAL and self.url_scheme not in ('file', ''):
|
||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||
raise ValidationError({
|
||||
'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
|
||||
})
|
||||
@ -368,7 +368,7 @@ class AutoSyncRecord(models.Model):
|
||||
related_name='+'
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+'
|
||||
)
|
||||
@ -378,6 +378,8 @@ class AutoSyncRecord(models.Model):
|
||||
fk_field='object_id'
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
|
@ -45,6 +45,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
ordering = ('file_root', 'file_path')
|
||||
|
@ -3,7 +3,7 @@ import uuid
|
||||
import django_rq
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@ -11,12 +11,13 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ContentType
|
||||
from core.signals import job_end, job_start
|
||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.rqworker import get_queue_for_model, get_rq_retry
|
||||
from utilities.rqworker import get_queue_for_model
|
||||
|
||||
__all__ = (
|
||||
'Job',
|
||||
@ -28,9 +29,8 @@ class Job(models.Model):
|
||||
Tracks the lifecycle of a job which represents a background task (e.g. the execution of a custom script).
|
||||
"""
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='jobs',
|
||||
limit_choices_to=FeatureQuery('jobs'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField(
|
||||
@ -92,6 +92,11 @@ class Job(models.Model):
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
error = models.TextField(
|
||||
verbose_name=_('error'),
|
||||
editable=False,
|
||||
blank=True
|
||||
)
|
||||
job_id = models.UUIDField(
|
||||
verbose_name=_('job ID'),
|
||||
unique=True
|
||||
@ -101,6 +106,9 @@ class Job(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('job')
|
||||
verbose_name_plural = _('jobs')
|
||||
|
||||
@ -118,6 +126,15 @@ class Job(models.Model):
|
||||
def get_status_color(self):
|
||||
return JobStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.object_type not in ContentType.objects.with_feature('jobs'):
|
||||
raise ValidationError(
|
||||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
if not self.completed:
|
||||
@ -155,10 +172,10 @@ class Job(models.Model):
|
||||
self.status = JobStatusChoices.STATUS_RUNNING
|
||||
self.save()
|
||||
|
||||
# Handle webhooks
|
||||
self.trigger_webhooks(event=EVENT_JOB_START)
|
||||
# Send signal
|
||||
job_start.send(self)
|
||||
|
||||
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED):
|
||||
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
|
||||
"""
|
||||
Mark the job as completed, optionally specifying a particular termination status.
|
||||
"""
|
||||
@ -168,11 +185,13 @@ class Job(models.Model):
|
||||
|
||||
# Mark the job as completed
|
||||
self.status = status
|
||||
if error:
|
||||
self.error = error
|
||||
self.completed = timezone.now()
|
||||
self.save()
|
||||
|
||||
# Handle webhooks
|
||||
self.trigger_webhooks(event=EVENT_JOB_END)
|
||||
# Send signal
|
||||
job_end.send(self)
|
||||
|
||||
@classmethod
|
||||
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
|
||||
@ -208,28 +227,3 @@ class Job(models.Model):
|
||||
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
return job
|
||||
|
||||
def trigger_webhooks(self, event):
|
||||
from extras.models import Webhook
|
||||
|
||||
rq_queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
|
||||
rq_queue = django_rq.get_queue(rq_queue_name, is_async=False)
|
||||
|
||||
# Fetch any webhooks matching this object type and action
|
||||
webhooks = Webhook.objects.filter(
|
||||
**{f'type_{event}': True},
|
||||
content_types=self.object_type,
|
||||
enabled=True
|
||||
)
|
||||
|
||||
for webhook in webhooks:
|
||||
rq_queue.enqueue(
|
||||
"extras.webhooks_worker.process_webhook",
|
||||
webhook=webhook,
|
||||
model_name=self.object_type.model,
|
||||
event=event,
|
||||
data=self.data,
|
||||
timestamp=timezone.now().isoformat(),
|
||||
username=self.user.username,
|
||||
retry=get_rq_retry()
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ class DataSourceIndex(SearchIndex):
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('type', 'status', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@ -19,3 +20,4 @@ class DataFileIndex(SearchIndex):
|
||||
fields = (
|
||||
('path', 200),
|
||||
)
|
||||
display_attrs = ('source',)
|
||||
|
@ -1,10 +1,19 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import Signal, receiver
|
||||
|
||||
from .models import ConfigRevision
|
||||
|
||||
__all__ = (
|
||||
'job_end',
|
||||
'job_start',
|
||||
'post_sync',
|
||||
'pre_sync',
|
||||
)
|
||||
|
||||
# Job signals
|
||||
job_start = Signal()
|
||||
job_end = Signal()
|
||||
|
||||
# DataSource signals
|
||||
pre_sync = Signal()
|
||||
post_sync = Signal()
|
||||
@ -19,3 +28,11 @@ def auto_sync(instance, **kwargs):
|
||||
|
||||
for autosync in AutoSyncRecord.objects.filter(datafile__source=instance).prefetch_related('object'):
|
||||
autosync.object.sync(save=True)
|
||||
|
||||
|
||||
@receiver(post_save, sender=ConfigRevision)
|
||||
def update_config(sender, instance, **kwargs):
|
||||
"""
|
||||
Update the cached NetBox configuration when a new ConfigRevision is created.
|
||||
"""
|
||||
instance.activate()
|
||||
|
@ -1,2 +1,3 @@
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .jobs import *
|
||||
|
20
netbox/core/tables/columns.py
Normal file
20
netbox/core/tables/columns.py
Normal file
@ -0,0 +1,20 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
__all__ = (
|
||||
'BackendTypeColumn',
|
||||
)
|
||||
|
||||
|
||||
class BackendTypeColumn(tables.Column):
|
||||
"""
|
||||
Display a data backend type.
|
||||
"""
|
||||
def render(self, value):
|
||||
if backend := registry['data_backends'].get(value):
|
||||
return backend.label
|
||||
return value
|
||||
|
||||
def value(self, value):
|
||||
return value
|
33
netbox/core/tables/config.py
Normal file
33
netbox/core/tables/config.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ConfigRevision
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevisionTable',
|
||||
)
|
||||
|
||||
REVISION_BUTTONS = """
|
||||
{% if not record.is_active %}
|
||||
<a href="{% url 'core:configrevision_restore' pk=record.pk %}" class="btn btn-sm btn-primary" title="Restore config">
|
||||
<i class="mdi mdi-file-restore"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
class ConfigRevisionTable(NetBoxTable):
|
||||
is_active = columns.BooleanColumn(
|
||||
verbose_name=_('Is Active'),
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('delete',),
|
||||
extra_buttons=REVISION_BUTTONS
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ConfigRevision
|
||||
fields = (
|
||||
'pk', 'id', 'is_active', 'created', 'comment',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'is_active', 'created', 'comment')
|
@ -3,6 +3,7 @@ import django_tables2 as tables
|
||||
|
||||
from core.models import *
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from .columns import BackendTypeColumn
|
||||
|
||||
__all__ = (
|
||||
'DataFileTable',
|
||||
@ -15,8 +16,8 @@ class DataSourceTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
type = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Type'),
|
||||
type = BackendTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
@ -34,8 +35,8 @@ class DataSourceTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = DataSource
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters', 'created',
|
||||
'last_updated', 'file_count',
|
||||
'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters',
|
||||
'created', 'last_updated', 'file_count',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'file_count')
|
||||
|
||||
|
@ -48,7 +48,7 @@ class JobTable(NetBoxTable):
|
||||
model = Job
|
||||
fields = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
||||
'completed', 'user', 'job_id',
|
||||
'completed', 'user', 'error', 'job_id',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||
|
@ -2,7 +2,6 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
from ..choices import *
|
||||
from ..models import *
|
||||
|
||||
|
||||
@ -26,26 +25,26 @@ class DataSourceTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
data_sources = (
|
||||
DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
|
||||
DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
|
||||
)
|
||||
DataSource.objects.bulk_create(data_sources)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Data Source 4',
|
||||
'type': DataSourceTypeChoices.GIT,
|
||||
'type': 'git',
|
||||
'source_url': 'https://example.com/git/source4'
|
||||
},
|
||||
{
|
||||
'name': 'Data Source 5',
|
||||
'type': DataSourceTypeChoices.GIT,
|
||||
'type': 'git',
|
||||
'source_url': 'https://example.com/git/source5'
|
||||
},
|
||||
{
|
||||
'name': 'Data Source 6',
|
||||
'type': DataSourceTypeChoices.GIT,
|
||||
'type': 'git',
|
||||
'source_url': 'https://example.com/git/source6'
|
||||
},
|
||||
]
|
||||
@ -63,7 +62,7 @@ class DataFileTest(
|
||||
def setUpTestData(cls):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source1/'
|
||||
)
|
||||
|
||||
|
@ -18,7 +18,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
data_sources = (
|
||||
DataSource(
|
||||
name='Data Source 1',
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source1/',
|
||||
status=DataSourceStatusChoices.NEW,
|
||||
enabled=True,
|
||||
@ -26,7 +26,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 2',
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source2/',
|
||||
status=DataSourceStatusChoices.SYNCING,
|
||||
enabled=True,
|
||||
@ -34,7 +34,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 3',
|
||||
type=DataSourceTypeChoices.GIT,
|
||||
type='git',
|
||||
source_url='https://example.com/git/source3',
|
||||
status=DataSourceStatusChoices.COMPLETED,
|
||||
enabled=False
|
||||
@ -55,7 +55,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
params = {'type': [DataSourceTypeChoices.LOCAL]}
|
||||
params = {'type': ['local']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_enabled(self):
|
||||
@ -76,9 +76,9 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
data_sources = (
|
||||
DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
|
||||
DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
|
||||
)
|
||||
DataSource.objects.bulk_create(data_sources)
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.utils import timezone
|
||||
|
||||
from utilities.testing import ViewTestCases, create_tags
|
||||
from ..choices import *
|
||||
from ..models import *
|
||||
|
||||
|
||||
@ -11,9 +10,9 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
data_sources = (
|
||||
DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
|
||||
DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
|
||||
DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
|
||||
DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
|
||||
)
|
||||
DataSource.objects.bulk_create(data_sources)
|
||||
|
||||
@ -21,7 +20,7 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Data Source X',
|
||||
'type': DataSourceTypeChoices.GIT,
|
||||
'type': 'git',
|
||||
'source_url': 'http:///exmaple/com/foo/bar/',
|
||||
'description': 'Something',
|
||||
'comments': 'Foo bar baz',
|
||||
@ -29,10 +28,10 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
f"name,type,source_url,enabled",
|
||||
f"Data Source 4,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true",
|
||||
f"Data Source 5,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true",
|
||||
f"Data Source 6,{DataSourceTypeChoices.GIT},http:///exmaple/com/foo/bar/,false",
|
||||
"name,type,source_url,enabled",
|
||||
"Data Source 4,local,file:///var/tmp/source4/,true",
|
||||
"Data Source 5,local,file:///var/tmp/source4/,true",
|
||||
"Data Source 6,git,http:///exmaple/com/foo/bar/,false",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
@ -60,7 +59,7 @@ class DataFileTestCase(
|
||||
def setUpTestData(cls):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source1/'
|
||||
)
|
||||
|
||||
|
@ -25,6 +25,13 @@ urlpatterns = (
|
||||
path('jobs/<int:pk>/', views.JobView.as_view(), name='job'),
|
||||
path('jobs/<int:pk>/delete/', views.JobDeleteView.as_view(), name='job_delete'),
|
||||
|
||||
# Config revisions
|
||||
path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
|
||||
path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
|
||||
path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
|
||||
path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
|
||||
path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
|
||||
|
||||
# Configuration
|
||||
path('config/', views.ConfigView.as_view(), name='config'),
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.models import ConfigRevision
|
||||
from netbox.config import get_config
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import register_model_view
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
||||
@ -101,7 +102,9 @@ class DataFileListView(generic.ObjectListView):
|
||||
filterset = filtersets.DataFileFilterSet
|
||||
filterset_form = forms.DataFileFilterForm
|
||||
table = tables.DataFileTable
|
||||
actions = ('bulk_delete',)
|
||||
actions = {
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(DataFile)
|
||||
@ -129,7 +132,10 @@ class JobListView(generic.ObjectListView):
|
||||
filterset = filtersets.JobFilterSet
|
||||
filterset_form = forms.JobFilterForm
|
||||
table = tables.JobTable
|
||||
actions = ('export', 'delete', 'bulk_delete')
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
|
||||
|
||||
class JobView(generic.ObjectView):
|
||||
@ -162,3 +168,67 @@ class ConfigView(generic.ObjectView):
|
||||
return ConfigRevision(
|
||||
data=get_config()
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionListView(generic.ObjectListView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
filterset = filtersets.ConfigRevisionFilterSet
|
||||
filterset_form = forms.ConfigRevisionFilterForm
|
||||
table = tables.ConfigRevisionTable
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision)
|
||||
class ConfigRevisionView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
|
||||
class ConfigRevisionEditView(generic.ObjectEditView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
form = forms.ConfigRevisionForm
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'delete')
|
||||
class ConfigRevisionDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
|
||||
class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
filterset = filtersets.ConfigRevisionFilterSet
|
||||
table = tables.ConfigRevisionTable
|
||||
|
||||
|
||||
class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
|
||||
def get_required_permission(self):
|
||||
return 'core.configrevision_edit'
|
||||
|
||||
def get(self, request, pk):
|
||||
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
|
||||
|
||||
# Get the current ConfigRevision
|
||||
config_version = get_config().version
|
||||
current_config = ConfigRevision.objects.filter(pk=config_version).first()
|
||||
|
||||
params = []
|
||||
for param in PARAMS:
|
||||
params.append((
|
||||
param.name,
|
||||
current_config.data.get(param.name, None),
|
||||
candidate_config.data.get(param.name, None)
|
||||
))
|
||||
|
||||
return render(request, 'core/configrevision_restore.html', {
|
||||
'object': candidate_config,
|
||||
'params': params,
|
||||
})
|
||||
|
||||
def post(self, request, pk):
|
||||
if not request.user.has_perm('core.configrevision_edit'):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
|
||||
candidate_config.activate()
|
||||
messages.success(request, f"Restored configuration revision #{pk}")
|
||||
|
||||
return redirect(candidate_config.get_absolute_url())
|
||||
|
@ -2,8 +2,8 @@ import decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
@ -12,8 +12,7 @@ from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
||||
from ipam.api.nested_serializers import (
|
||||
NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
|
||||
NestedVRFSerializer,
|
||||
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
||||
)
|
||||
from ipam.models import ASN, VLAN
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
@ -27,6 +26,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
|
||||
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
|
||||
from wireless.choices import *
|
||||
from wireless.models import WirelessLAN
|
||||
@ -343,9 +343,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
|
||||
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
|
||||
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
|
||||
'power_outlet_template_count', 'interface_template_count', 'front_port_template_count',
|
||||
'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count',
|
||||
'inventory_item_template_count',
|
||||
|
@ -3,10 +3,8 @@ from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
@ -14,16 +12,16 @@ from dcim import filtersets
|
||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||
from dcim.models import *
|
||||
from dcim.svg import CableTraceSVG
|
||||
from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
|
||||
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||
from netbox.api.renderers import TextRenderer
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
|
||||
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
@ -389,7 +387,7 @@ class PlatformViewSet(NetBoxModelViewSet):
|
||||
class DeviceViewSet(
|
||||
SequentialBulkCreatesMixin,
|
||||
ConfigContextQuerySetMixin,
|
||||
ConfigTemplateRenderMixin,
|
||||
RenderConfigMixin,
|
||||
NetBoxModelViewSet
|
||||
):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
@ -419,23 +417,6 @@ class DeviceViewSet(
|
||||
|
||||
return serializers.DeviceWithConfigContextSerializer
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
|
||||
def render_config(self, request, pk):
|
||||
"""
|
||||
Resolve and render the preferred ConfigTemplate for this Device.
|
||||
"""
|
||||
device = self.get_object()
|
||||
configtemplate = device.get_config_template()
|
||||
if not configtemplate:
|
||||
return Response({'error': 'No config template found for this device.'}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Compile context data
|
||||
context_data = device.get_config_context()
|
||||
context_data.update(request.data)
|
||||
context_data.update({'device': device})
|
||||
|
||||
return self.render_configtemplate(request, configtemplate, context_data)
|
||||
|
||||
|
||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
||||
@ -505,6 +486,10 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
def get_bulk_destroy_queryset(self):
|
||||
# Ensure child interfaces are deleted prior to their parents
|
||||
return self.get_queryset().order_by('device', 'parent', CollateAsChar('_name'))
|
||||
|
||||
|
||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = FrontPort.objects.prefetch_related(
|
||||
|
@ -80,10 +80,10 @@ class RackWidthChoices(ChoiceSet):
|
||||
WIDTH_23IN = 23
|
||||
|
||||
CHOICES = (
|
||||
(WIDTH_10IN, _('10 inches')),
|
||||
(WIDTH_19IN, _('19 inches')),
|
||||
(WIDTH_21IN, _('21 inches')),
|
||||
(WIDTH_23IN, _('23 inches')),
|
||||
(WIDTH_10IN, _('{n} inches').format(n=10)),
|
||||
(WIDTH_19IN, _('{n} inches').format(n=19)),
|
||||
(WIDTH_21IN, _('{n} inches').format(n=21)),
|
||||
(WIDTH_23IN, _('{n} inches').format(n=23)),
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import django_filters
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from ipam.models import ASN, L2VPN, IPAddress, VRF
|
||||
from ipam.models import ASN, IPAddress, VRF
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
)
|
||||
@ -17,6 +19,7 @@ from utilities.filters import (
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from vpn.models import L2VPN
|
||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
@ -498,8 +501,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
|
||||
'weight_unit', 'description',
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
|
||||
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -1803,6 +1806,35 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
field_name='site__slug'
|
||||
)
|
||||
|
||||
# Termination object filters
|
||||
consoleport_id = MultiValueNumberFilter(
|
||||
method='filter_by_consoleport'
|
||||
)
|
||||
consoleserverport_id = MultiValueNumberFilter(
|
||||
method='filter_by_consoleserverport'
|
||||
)
|
||||
powerport_id = MultiValueNumberFilter(
|
||||
method='filter_by_powerport'
|
||||
)
|
||||
poweroutlet_id = MultiValueNumberFilter(
|
||||
method='filter_by_poweroutlet'
|
||||
)
|
||||
interface_id = MultiValueNumberFilter(
|
||||
method='filter_by_interface'
|
||||
)
|
||||
frontport_id = MultiValueNumberFilter(
|
||||
method='filter_by_frontport'
|
||||
)
|
||||
rearport_id = MultiValueNumberFilter(
|
||||
method='filter_by_rearport'
|
||||
)
|
||||
powerfeed_id = MultiValueNumberFilter(
|
||||
method='filter_by_powerfeed'
|
||||
)
|
||||
circuittermination_id = MultiValueNumberFilter(
|
||||
method='filter_by_circuittermination'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'description']
|
||||
@ -1846,6 +1878,42 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
terminations__cable_end=CableEndChoices.SIDE_B
|
||||
)
|
||||
|
||||
def filter_by_termination_object(self, queryset, model, value):
|
||||
# Filter by specific termination object(s)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
cable_ids = CableTermination.objects.filter(
|
||||
termination_type=content_type,
|
||||
termination_id__in=value
|
||||
).values_list('cable', flat=True)
|
||||
return queryset.filter(pk__in=cable_ids)
|
||||
|
||||
def filter_by_consoleport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, ConsolePort, value)
|
||||
|
||||
def filter_by_consoleserverport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, ConsoleServerPort, value)
|
||||
|
||||
def filter_by_powerport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerPort, value)
|
||||
|
||||
def filter_by_poweroutlet(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerOutlet, value)
|
||||
|
||||
def filter_by_interface(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, Interface, value)
|
||||
|
||||
def filter_by_frontport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, FrontPort, value)
|
||||
|
||||
def filter_by_rearport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, RearPort, value)
|
||||
|
||||
def filter_by_powerfeed(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerFeed, value)
|
||||
|
||||
def filter_by_circuittermination(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
||||
|
||||
|
||||
class CableTerminationFilterSet(BaseFilterSet):
|
||||
termination_type = ContentTypeFilter()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user