mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-08 16:48:16 -06:00
Merge branch 'feature' into 12826-rack-type
This commit is contained in:
commit
cb296ce047
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -26,7 +26,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.0.6
|
placeholder: v4.0.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.0.6
|
placeholder: v4.0.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -31,6 +31,17 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## SENTRY_SEND_DEFAULT_PII
|
||||||
|
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
Maps to the Sentry SDK's [`send_default_pii`](https://docs.sentry.io/platforms/python/configuration/options/#send-default-pii) parameter. If enabled, certain personally identifiable information (PII) is added.
|
||||||
|
|
||||||
|
!!! warning "Sensitive data"
|
||||||
|
If you enable this option, be aware that sensitive data such as cookies and authentication tokens will be logged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## SENTRY_TAGS
|
## SENTRY_TAGS
|
||||||
|
|
||||||
An optional dictionary of tag names and values to apply to Sentry error reports.For example:
|
An optional dictionary of tag names and values to apply to Sentry error reports.For example:
|
||||||
|
@ -177,7 +177,7 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
|
|||||||
|
|
||||||
Default: None (local storage)
|
Default: None (local storage)
|
||||||
|
|
||||||
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
||||||
|
|
||||||
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
|
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ The configuration parameters for the specified storage backend are defined under
|
|||||||
|
|
||||||
Default: Empty
|
Default: Empty
|
||||||
|
|
||||||
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
|
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail.
|
||||||
|
|
||||||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
||||||
|
|
||||||
|
@ -135,4 +135,6 @@ First, run the `build-site` action, by navigating to Actions > build-site > Run
|
|||||||
|
|
||||||
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
|
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
|
||||||
|
|
||||||
|
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _CDN_ in the left-nav, click the _Clear CDN cache_ button, and confirm the clear operation.
|
||||||
|
|
||||||
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.
|
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.
|
||||||
|
@ -191,7 +191,7 @@ class MyView(generic.ObjectView):
|
|||||||
|
|
||||||
### Extra Template Content
|
### Extra Template Content
|
||||||
|
|
||||||
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired method(s) to render custom content. Five methods are available:
|
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, optionally designating one or more particular NetBox models, and defining the desired method(s) to render custom content. Five methods are available:
|
||||||
|
|
||||||
| Method | View | Description |
|
| Method | View | Description |
|
||||||
|---------------------|-------------|-----------------------------------------------------|
|
|---------------------|-------------|-----------------------------------------------------|
|
||||||
@ -206,7 +206,9 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
|||||||
|
|
||||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||||
|
|
||||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.
|
||||||
|
|
||||||
|
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data includes:
|
||||||
|
|
||||||
* `object` - The object being viewed (object views only)
|
* `object` - The object being viewed (object views only)
|
||||||
* `model` - The model of the list view (list views only)
|
* `model` - The model of the list view (list views only)
|
||||||
@ -223,7 +225,7 @@ from netbox.plugins import PluginTemplateExtension
|
|||||||
from .models import Animal
|
from .models import Animal
|
||||||
|
|
||||||
class SiteAnimalCount(PluginTemplateExtension):
|
class SiteAnimalCount(PluginTemplateExtension):
|
||||||
model = 'dcim.site'
|
models = ['dcim.site']
|
||||||
|
|
||||||
def right_page(self):
|
def right_page(self):
|
||||||
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
|
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
|
||||||
|
@ -70,3 +70,19 @@ DROP TABLE
|
|||||||
netbox=> DROP TABLE pluginname_bar;
|
netbox=> DROP TABLE pluginname_bar;
|
||||||
DROP TABLE
|
DROP TABLE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Remove the Django Migration Records
|
||||||
|
|
||||||
|
After removing the tables created by a plugin, the migrations that created the tables need to be removed from Django's migration history as well. This is necessary to make it possible to reinstall the plugin at a later time. If the migration history were left in place, Django would skip all migrations that were executed in the course of a previous installation, which would cause the plugin to fail after reinstallation.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
netbox=> SELECT * FROM django_migrations WHERE app='pluginname';
|
||||||
|
id | app | name | applied
|
||||||
|
-----+------------+------------------------+-------------------------------
|
||||||
|
492 | pluginname | 0001_initial | 2023-12-21 11:59:59.325995+00
|
||||||
|
493 | pluginname | 0002_add_foo | 2023-12-21 11:59:59.330026+00
|
||||||
|
netbox=> DELETE FROM django_migrations WHERE app='pluginname';
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||||
|
@ -1,6 +1,42 @@
|
|||||||
# NetBox v4.0
|
# NetBox v4.0
|
||||||
|
|
||||||
## v4.0.7 (FUTURE)
|
## v4.0.8 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.0.7 (2024-07-09)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#14554](https://github.com/netbox-community/netbox/issues/14554) - Add support for [django-storage-swift](https://github.com/dennisv/django-storage-swift) storage backend
|
||||||
|
* [#16424](https://github.com/netbox-community/netbox/issues/16424) - Enable filtering of devices by cluster and cluster group
|
||||||
|
* [#16716](https://github.com/netbox-community/netbox/issues/16716) - Display NAT address (if any) for OOB IP address under device view
|
||||||
|
* [#16725](https://github.com/netbox-community/netbox/issues/16725) - Always position the admin section last in the navigation menu
|
||||||
|
* [#16791](https://github.com/netbox-community/netbox/issues/16791) - Add 200 & 400 Gbps selections for circuit termination port speed
|
||||||
|
* [#16802](https://github.com/netbox-community/netbox/issues/16802) - Introduce `SENTRY_SEND_DEFAULT_PII` configuration parameter and disable PII export by default
|
||||||
|
* [#16817](https://github.com/netbox-community/netbox/issues/16817) - Add 200 & 400 Gbps selections for circuit commit rate
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#16523](https://github.com/netbox-community/netbox/issues/16523) - Restore highlighting of current device in virtual chassis members panel
|
||||||
|
* [#16654](https://github.com/netbox-community/netbox/issues/16654) - Fix parent item assignment for inventory item bulk import
|
||||||
|
* [#16657](https://github.com/netbox-community/netbox/issues/16657) - Fix translation of object types in global search
|
||||||
|
* [#16679](https://github.com/netbox-community/netbox/issues/16679) - Avoid overwriting custom JSON fields during bulk edit
|
||||||
|
* [#16689](https://github.com/netbox-community/netbox/issues/16689) - System configuration view should reflect static parameters when no config revisions exist
|
||||||
|
* [#16714](https://github.com/netbox-community/netbox/issues/16714) - Fix cloning of device types with 0U height
|
||||||
|
* [#16721](https://github.com/netbox-community/netbox/issues/16721) - Fix errant API request after deselecting a rack in device edit form
|
||||||
|
* [#16723](https://github.com/netbox-community/netbox/issues/16723) - Fix escaping of path to virtual environment in `upgrade.sh`
|
||||||
|
* [#16735](https://github.com/netbox-community/netbox/issues/16735) - Object list "results" tab should show a count of zero when empty
|
||||||
|
* [#16747](https://github.com/netbox-community/netbox/issues/16747) - Avoid clearing entire search cache when manually reindexing specific apps/models
|
||||||
|
* [#16758](https://github.com/netbox-community/netbox/issues/16758) - Ensure manually selected lagnuage persists across browser sessions
|
||||||
|
* [#16779](https://github.com/netbox-community/netbox/issues/16779) - Fix saved filter selection for child object lists
|
||||||
|
* [#16780](https://github.com/netbox-community/netbox/issues/16780) - IKE proposal created via REST API should not require authentication_algorithm
|
||||||
|
* [#16796](https://github.com/netbox-community/netbox/issues/16796) - Allow assignment of VM with no site to a cluster with a site
|
||||||
|
* [#16806](https://github.com/netbox-community/netbox/issues/16806) - Fix redirect URL when creating contact assignments with "add another" button
|
||||||
|
* [#16807](https://github.com/netbox-community/netbox/issues/16807) - Fix layout of VLAN edit form when custom fields are present
|
||||||
|
* [#16808](https://github.com/netbox-community/netbox/issues/16808) - Fix event rule triggering in scenario where objects are updated immediately prior to deletion
|
||||||
|
* [#16813](https://github.com/netbox-community/netbox/issues/16813) - Fix AttributeError exception when filtering bookmarks in dashboard widget by object type
|
||||||
|
* [#16843](https://github.com/netbox-community/netbox/issues/16843) - Permit creation of IKE policies via REST API without specifying an IKE mode
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -12,7 +12,14 @@
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
|
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
|
||||||
|
* [#8984](https://github.com/netbox-community/netbox/issues/8984) - Enable filtering of custom script output by log level
|
||||||
|
* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers
|
||||||
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
|
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
|
||||||
|
* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable individual views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
* [#16726](https://github.com/netbox-community/netbox/issues/16726) - Extend `PluginTemplateExtension` to enable registering multiple models
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ class LoginView(View):
|
|||||||
|
|
||||||
# Set the user's preferred language (if any)
|
# Set the user's preferred language (if any)
|
||||||
if language := request.user.config.get('locale.language'):
|
if language := request.user.config.get('locale.language'):
|
||||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ class UserConfigView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
# Set/clear language cookie
|
# Set/clear language cookie
|
||||||
if language := form.cleaned_data['locale.language']:
|
if language := form.cleaned_data['locale.language']:
|
||||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||||
else:
|
else:
|
||||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||||
|
|
||||||
|
@ -38,6 +38,8 @@ class CircuitCommitRateChoices(ChoiceSet):
|
|||||||
(25000000, '25 Gbps'),
|
(25000000, '25 Gbps'),
|
||||||
(40000000, '40 Gbps'),
|
(40000000, '40 Gbps'),
|
||||||
(100000000, '100 Gbps'),
|
(100000000, '100 Gbps'),
|
||||||
|
(200000000, '200 Gbps'),
|
||||||
|
(400000000, '400 Gbps'),
|
||||||
(1544, 'T1 (1.544 Mbps)'),
|
(1544, 'T1 (1.544 Mbps)'),
|
||||||
(2048, 'E1 (2.048 Mbps)'),
|
(2048, 'E1 (2.048 Mbps)'),
|
||||||
]
|
]
|
||||||
@ -69,6 +71,8 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
|||||||
(25000000, '25 Gbps'),
|
(25000000, '25 Gbps'),
|
||||||
(40000000, '40 Gbps'),
|
(40000000, '40 Gbps'),
|
||||||
(100000000, '100 Gbps'),
|
(100000000, '100 Gbps'),
|
||||||
|
(200000000, '200 Gbps'),
|
||||||
|
(400000000, '400 Gbps'),
|
||||||
(1544, 'T1 (1.544 Mbps)'),
|
(1544, 'T1 (1.544 Mbps)'),
|
||||||
(2048, 'E1 (2.048 Mbps)'),
|
(2048, 'E1 (2.048 Mbps)'),
|
||||||
]
|
]
|
||||||
|
@ -66,9 +66,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(NetBoxModelImportForm):
|
class CircuitImportForm(NetBoxModelImportForm):
|
||||||
|
@ -625,7 +625,7 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
|
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
|
||||||
except ConfigRevision.DoesNotExist:
|
except ConfigRevision.DoesNotExist:
|
||||||
# Fall back to using the active config data if no record is found
|
# Fall back to using the active config data if no record is found
|
||||||
config = ConfigRevision(data=get_config().defaults)
|
config = get_config()
|
||||||
|
|
||||||
# Raw data export
|
# Raw data export
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
|
@ -20,7 +20,7 @@ from utilities.filters import (
|
|||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from vpn.models import L2VPN
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
@ -1048,6 +1048,17 @@ class DeviceFilterSet(
|
|||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
label=_('VM cluster (ID)'),
|
label=_('VM cluster (ID)'),
|
||||||
)
|
)
|
||||||
|
cluster_group = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='cluster__group__slug',
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Cluster group (slug)'),
|
||||||
|
)
|
||||||
|
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='cluster__group',
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
label=_('Cluster group (ID)'),
|
||||||
|
)
|
||||||
model = django_filters.ModelMultipleChoiceFilter(
|
model = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device_type__slug',
|
field_name='device_type__slug',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
|
@ -175,9 +175,6 @@ class RackRoleImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RackTypeImportForm(NetBoxModelImportForm):
|
class RackTypeImportForm(NetBoxModelImportForm):
|
||||||
@ -422,9 +419,6 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
|
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PlatformImportForm(NetBoxModelImportForm):
|
class PlatformImportForm(NetBoxModelImportForm):
|
||||||
@ -1090,7 +1084,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
'device', 'name', 'label', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
'description', 'tags', 'component_type', 'component_name',
|
'description', 'tags', 'component_type', 'component_name',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1142,9 +1136,6 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = ('name', 'slug', 'color', 'description')
|
fields = ('name', 'slug', 'color', 'description')
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1221,9 +1212,6 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
||||||
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
|
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _clean_side(self, side):
|
def _clean_side(self, side):
|
||||||
"""
|
"""
|
||||||
|
@ -14,6 +14,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch
|
|||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import NumberWithOptions
|
from utilities.forms.widgets import NumberWithOptions
|
||||||
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from vpn.models import L2VPN
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
|
|
||||||
@ -692,6 +693,7 @@ class DeviceFilterForm(
|
|||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||||
name=_('Components')
|
name=_('Components')
|
||||||
),
|
),
|
||||||
|
FieldSet('cluster_group_id', 'cluster_id', name=_('Cluster')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
|
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
|
||||||
'has_virtual_device_context',
|
'has_virtual_device_context',
|
||||||
@ -858,6 +860,16 @@ class DeviceFilterForm(
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Cluster')
|
||||||
|
)
|
||||||
|
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Cluster group')
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from ipam.models import ASN, IPAddress, RIR, VRF
|
|||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType, ClusterGroup
|
||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -2081,10 +2081,16 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
cluster_groups = (
|
||||||
|
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
|
||||||
|
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
|
||||||
|
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
|
||||||
|
)
|
||||||
|
ClusterGroup.objects.bulk_create(cluster_groups)
|
||||||
clusters = (
|
clusters = (
|
||||||
Cluster(name='Cluster 1', type=cluster_type),
|
Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0]),
|
||||||
Cluster(name='Cluster 2', type=cluster_type),
|
Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1]),
|
||||||
Cluster(name='Cluster 3', type=cluster_type),
|
Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2]),
|
||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
@ -2335,6 +2341,13 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
|
params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_cluster_group(self):
|
||||||
|
cluster_groups = ClusterGroup.objects.all()[:2]
|
||||||
|
params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_model(self):
|
def test_model(self):
|
||||||
params = {'model': ['model-1', 'model-2']}
|
params = {'model': ['model-1', 'model-2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -31,6 +31,7 @@ from utilities.views import (
|
|||||||
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||||
)
|
)
|
||||||
from virtualization.filtersets import VirtualMachineFilterSet
|
from virtualization.filtersets import VirtualMachineFilterSet
|
||||||
|
from virtualization.forms import VirtualMachineFilterForm
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from virtualization.tables import VirtualMachineTable
|
from virtualization.tables import VirtualMachineTable
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
@ -725,6 +726,7 @@ class RackRackReservationsView(generic.ObjectChildrenView):
|
|||||||
child_model = RackReservation
|
child_model = RackReservation
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
filterset = filtersets.RackReservationFilterSet
|
filterset = filtersets.RackReservationFilterSet
|
||||||
|
filterset_form = forms.RackReservationFilterForm
|
||||||
template_name = 'dcim/rack/reservations.html'
|
template_name = 'dcim/rack/reservations.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Reservations'),
|
label=_('Reservations'),
|
||||||
@ -743,6 +745,7 @@ class RackNonRackedView(generic.ObjectChildrenView):
|
|||||||
child_model = Device
|
child_model = Device
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
|
filterset_form = forms.DeviceFilterForm
|
||||||
template_name = 'dcim/rack/non_racked_devices.html'
|
template_name = 'dcim/rack/non_racked_devices.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Non-Racked Devices'),
|
label=_('Non-Racked Devices'),
|
||||||
@ -1881,6 +1884,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
|
|||||||
child_model = ConsolePort
|
child_model = ConsolePort
|
||||||
table = tables.DeviceConsolePortTable
|
table = tables.DeviceConsolePortTable
|
||||||
filterset = filtersets.ConsolePortFilterSet
|
filterset = filtersets.ConsolePortFilterSet
|
||||||
|
filterset_form = forms.ConsolePortFilterForm
|
||||||
template_name = 'dcim/device/consoleports.html',
|
template_name = 'dcim/device/consoleports.html',
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Ports'),
|
label=_('Console Ports'),
|
||||||
@ -1896,6 +1900,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
|
|||||||
child_model = ConsoleServerPort
|
child_model = ConsoleServerPort
|
||||||
table = tables.DeviceConsoleServerPortTable
|
table = tables.DeviceConsoleServerPortTable
|
||||||
filterset = filtersets.ConsoleServerPortFilterSet
|
filterset = filtersets.ConsoleServerPortFilterSet
|
||||||
|
filterset_form = forms.ConsoleServerPortFilterForm
|
||||||
template_name = 'dcim/device/consoleserverports.html'
|
template_name = 'dcim/device/consoleserverports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Server Ports'),
|
label=_('Console Server Ports'),
|
||||||
@ -1911,6 +1916,7 @@ class DevicePowerPortsView(DeviceComponentsView):
|
|||||||
child_model = PowerPort
|
child_model = PowerPort
|
||||||
table = tables.DevicePowerPortTable
|
table = tables.DevicePowerPortTable
|
||||||
filterset = filtersets.PowerPortFilterSet
|
filterset = filtersets.PowerPortFilterSet
|
||||||
|
filterset_form = forms.PowerPortFilterForm
|
||||||
template_name = 'dcim/device/powerports.html'
|
template_name = 'dcim/device/powerports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Ports'),
|
label=_('Power Ports'),
|
||||||
@ -1926,6 +1932,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
|
|||||||
child_model = PowerOutlet
|
child_model = PowerOutlet
|
||||||
table = tables.DevicePowerOutletTable
|
table = tables.DevicePowerOutletTable
|
||||||
filterset = filtersets.PowerOutletFilterSet
|
filterset = filtersets.PowerOutletFilterSet
|
||||||
|
filterset_form = forms.PowerOutletFilterForm
|
||||||
template_name = 'dcim/device/poweroutlets.html'
|
template_name = 'dcim/device/poweroutlets.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Outlets'),
|
label=_('Power Outlets'),
|
||||||
@ -1941,6 +1948,7 @@ class DeviceInterfacesView(DeviceComponentsView):
|
|||||||
child_model = Interface
|
child_model = Interface
|
||||||
table = tables.DeviceInterfaceTable
|
table = tables.DeviceInterfaceTable
|
||||||
filterset = filtersets.InterfaceFilterSet
|
filterset = filtersets.InterfaceFilterSet
|
||||||
|
filterset_form = forms.InterfaceFilterForm
|
||||||
template_name = 'dcim/device/interfaces.html'
|
template_name = 'dcim/device/interfaces.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Interfaces'),
|
label=_('Interfaces'),
|
||||||
@ -1962,6 +1970,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
|
|||||||
child_model = FrontPort
|
child_model = FrontPort
|
||||||
table = tables.DeviceFrontPortTable
|
table = tables.DeviceFrontPortTable
|
||||||
filterset = filtersets.FrontPortFilterSet
|
filterset = filtersets.FrontPortFilterSet
|
||||||
|
filterset_form = forms.FrontPortFilterForm
|
||||||
template_name = 'dcim/device/frontports.html'
|
template_name = 'dcim/device/frontports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Front Ports'),
|
label=_('Front Ports'),
|
||||||
@ -1977,6 +1986,7 @@ class DeviceRearPortsView(DeviceComponentsView):
|
|||||||
child_model = RearPort
|
child_model = RearPort
|
||||||
table = tables.DeviceRearPortTable
|
table = tables.DeviceRearPortTable
|
||||||
filterset = filtersets.RearPortFilterSet
|
filterset = filtersets.RearPortFilterSet
|
||||||
|
filterset_form = forms.RearPortFilterForm
|
||||||
template_name = 'dcim/device/rearports.html'
|
template_name = 'dcim/device/rearports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Rear Ports'),
|
label=_('Rear Ports'),
|
||||||
@ -1992,6 +2002,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
|
|||||||
child_model = ModuleBay
|
child_model = ModuleBay
|
||||||
table = tables.DeviceModuleBayTable
|
table = tables.DeviceModuleBayTable
|
||||||
filterset = filtersets.ModuleBayFilterSet
|
filterset = filtersets.ModuleBayFilterSet
|
||||||
|
filterset_form = forms.ModuleBayFilterForm
|
||||||
template_name = 'dcim/device/modulebays.html'
|
template_name = 'dcim/device/modulebays.html'
|
||||||
actions = {
|
actions = {
|
||||||
**DEFAULT_ACTION_PERMISSIONS,
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
@ -2011,6 +2022,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
|||||||
child_model = DeviceBay
|
child_model = DeviceBay
|
||||||
table = tables.DeviceDeviceBayTable
|
table = tables.DeviceDeviceBayTable
|
||||||
filterset = filtersets.DeviceBayFilterSet
|
filterset = filtersets.DeviceBayFilterSet
|
||||||
|
filterset_form = forms.DeviceBayFilterForm
|
||||||
template_name = 'dcim/device/devicebays.html'
|
template_name = 'dcim/device/devicebays.html'
|
||||||
actions = {
|
actions = {
|
||||||
**DEFAULT_ACTION_PERMISSIONS,
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
@ -2030,6 +2042,7 @@ class DeviceInventoryView(DeviceComponentsView):
|
|||||||
child_model = InventoryItem
|
child_model = InventoryItem
|
||||||
table = tables.DeviceInventoryItemTable
|
table = tables.DeviceInventoryItemTable
|
||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
|
filterset_form = forms.InventoryItemFilterForm
|
||||||
template_name = 'dcim/device/inventory.html'
|
template_name = 'dcim/device/inventory.html'
|
||||||
actions = {
|
actions = {
|
||||||
**DEFAULT_ACTION_PERMISSIONS,
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
@ -2108,6 +2121,7 @@ class DeviceVirtualMachinesView(generic.ObjectChildrenView):
|
|||||||
child_model = VirtualMachine
|
child_model = VirtualMachine
|
||||||
table = VirtualMachineTable
|
table = VirtualMachineTable
|
||||||
filterset = VirtualMachineFilterSet
|
filterset = VirtualMachineFilterSet
|
||||||
|
filterset_form = VirtualMachineFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Virtual Machines'),
|
label=_('Virtual Machines'),
|
||||||
badge=lambda obj: VirtualMachine.objects.filter(cluster=obj.cluster, device=obj).count(),
|
badge=lambda obj: VirtualMachine.objects.filter(cluster=obj.cluster, device=obj).count(),
|
||||||
@ -2990,6 +3004,7 @@ class InventoryItemChildrenView(generic.ObjectChildrenView):
|
|||||||
child_model = InventoryItem
|
child_model = InventoryItem
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
|
filterset_form = forms.InventoryItemFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Children'),
|
label=_('Children'),
|
||||||
badge=lambda obj: obj.child_items.count(),
|
badge=lambda obj: obj.child_items.count(),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from extras.choices import LogLevelChoices
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
EVENT_CREATE = 'create'
|
EVENT_CREATE = 'create'
|
||||||
EVENT_UPDATE = 'update'
|
EVENT_UPDATE = 'update'
|
||||||
@ -135,3 +137,12 @@ DEFAULT_DASHBOARD = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LOG_LEVEL_RANK = {
|
||||||
|
LogLevelChoices.LOG_DEFAULT: 0,
|
||||||
|
LogLevelChoices.LOG_DEBUG: 1,
|
||||||
|
LogLevelChoices.LOG_SUCCESS: 2,
|
||||||
|
LogLevelChoices.LOG_INFO: 3,
|
||||||
|
LogLevelChoices.LOG_WARNING: 4,
|
||||||
|
LogLevelChoices.LOG_FAILURE: 5,
|
||||||
|
}
|
||||||
|
@ -381,17 +381,17 @@ class BookmarksWidget(DashboardWidget):
|
|||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
bookmarks = list()
|
bookmarks = list()
|
||||||
else:
|
else:
|
||||||
user_bookmarks = Bookmark.objects.filter(user=request.user)
|
bookmarks = Bookmark.objects.filter(user=request.user)
|
||||||
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
|
|
||||||
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
|
|
||||||
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
|
|
||||||
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
|
|
||||||
else:
|
|
||||||
bookmarks = user_bookmarks.order_by(self.config['order_by'])
|
|
||||||
if object_types := self.config.get('object_types'):
|
if object_types := self.config.get('object_types'):
|
||||||
models = get_models_from_content_types(object_types)
|
models = get_models_from_content_types(object_types)
|
||||||
content_types = ObjectType.objects.get_for_models(*models).values()
|
content_types = ObjectType.objects.get_for_models(*models).values()
|
||||||
bookmarks = bookmarks.filter(object_type__in=content_types)
|
bookmarks = bookmarks.filter(object_type__in=content_types)
|
||||||
|
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
|
||||||
|
bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower())
|
||||||
|
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
|
||||||
|
bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
|
||||||
|
else:
|
||||||
|
bookmarks = bookmarks.order_by(self.config['order_by'])
|
||||||
if max_items := self.config.get('max_items'):
|
if max_items := self.config.get('max_items'):
|
||||||
bookmarks = bookmarks[:max_items]
|
bookmarks = bookmarks[:max_items]
|
||||||
|
|
||||||
|
@ -66,6 +66,9 @@ def enqueue_object(queue, instance, user, request_id, action):
|
|||||||
if key in queue:
|
if key in queue:
|
||||||
queue[key]['data'] = serialize_for_event(instance)
|
queue[key]['data'] = serialize_for_event(instance)
|
||||||
queue[key]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
|
queue[key]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
|
||||||
|
# If the object is being deleted, update any prior "update" event to "delete"
|
||||||
|
if action == ObjectChangeActionChoices.ACTION_DELETE:
|
||||||
|
queue[key]['event'] = action
|
||||||
else:
|
else:
|
||||||
queue[key] = {
|
queue[key] = {
|
||||||
'content_type': ContentType.objects.get_for_model(instance),
|
'content_type': ContentType.objects.get_for_model(instance),
|
||||||
|
@ -229,9 +229,6 @@ class TagImportForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('name', 'slug', 'color', 'description')
|
fields = ('name', 'slug', 'color', 'description')
|
||||||
help_texts = {
|
|
||||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryImportForm(NetBoxModelImportForm):
|
class JournalEntryImportForm(NetBoxModelImportForm):
|
||||||
|
@ -66,11 +66,16 @@ class Command(BaseCommand):
|
|||||||
raise CommandError(_("No indexers found!"))
|
raise CommandError(_("No indexers found!"))
|
||||||
self.stdout.write(f'Reindexing {len(indexers)} models.')
|
self.stdout.write(f'Reindexing {len(indexers)} models.')
|
||||||
|
|
||||||
# Clear all cached values for the specified models (if not being lazy)
|
# Clear cached values for the specified models (if not being lazy)
|
||||||
if not kwargs['lazy']:
|
if not kwargs['lazy']:
|
||||||
|
if model_labels:
|
||||||
|
content_types = [ContentType.objects.get_for_model(model) for model in indexers.keys()]
|
||||||
|
else:
|
||||||
|
content_types = None
|
||||||
|
|
||||||
self.stdout.write('Clearing cached values... ', ending='')
|
self.stdout.write('Clearing cached values... ', ending='')
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
deleted_count = search_backend.clear()
|
deleted_count = search_backend.clear(object_types=content_types)
|
||||||
self.stdout.write(f'{deleted_count} entries deleted.')
|
self.stdout.write(f'{deleted_count} entries deleted.')
|
||||||
|
|
||||||
# Index models
|
# Index models
|
||||||
|
@ -501,7 +501,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# JSON
|
# JSON
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
||||||
field = JSONField(required=required, initial=json.dumps(initial) if initial else '')
|
field = JSONField(required=required, initial=json.dumps(initial) if initial else None)
|
||||||
|
|
||||||
# Object
|
# Object
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
@ -391,13 +391,36 @@ class EventRuleTest(APITestCase):
|
|||||||
request.id = uuid.uuid4()
|
request.id = uuid.uuid4()
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
self.assertEqual(self.queue.count, 0, msg="Unexpected jobs found in queue")
|
# Test create & update
|
||||||
|
|
||||||
with event_tracking(request):
|
with event_tracking(request):
|
||||||
site = Site(name='Site 1', slug='site-1')
|
site = Site(name='Site 1', slug='site-1')
|
||||||
site.save()
|
site.save()
|
||||||
|
site.description = 'foo'
|
||||||
# Save the site a second time
|
|
||||||
site.save()
|
site.save()
|
||||||
|
|
||||||
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
|
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
|
||||||
|
job = self.queue.get_jobs()[0]
|
||||||
|
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
|
||||||
|
self.queue.empty()
|
||||||
|
|
||||||
|
# Test multiple updates
|
||||||
|
site = Site.objects.create(name='Site 2', slug='site-2')
|
||||||
|
with event_tracking(request):
|
||||||
|
site.description = 'foo'
|
||||||
|
site.save()
|
||||||
|
site.description = 'bar'
|
||||||
|
site.save()
|
||||||
|
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
|
||||||
|
job = self.queue.get_jobs()[0]
|
||||||
|
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
|
||||||
|
self.queue.empty()
|
||||||
|
|
||||||
|
# Test update & delete
|
||||||
|
site = Site.objects.create(name='Site 3', slug='site-3')
|
||||||
|
with event_tracking(request):
|
||||||
|
site.description = 'foo'
|
||||||
|
site.save()
|
||||||
|
site.delete()
|
||||||
|
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
|
||||||
|
job = self.queue.get_jobs()[0]
|
||||||
|
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
|
||||||
|
self.queue.empty()
|
||||||
|
@ -14,6 +14,7 @@ from core.forms import ManagedFileForm
|
|||||||
from core.models import Job
|
from core.models import Job
|
||||||
from core.tables import JobTable
|
from core.tables import JobTable
|
||||||
from dcim.models import Device, DeviceRole, Platform
|
from dcim.models import Device, DeviceRole, Platform
|
||||||
|
from extras.choices import LogLevelChoices
|
||||||
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
||||||
from extras.dashboard.utils import get_widget_class
|
from extras.dashboard.utils import get_widget_class
|
||||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
@ -30,6 +31,7 @@ from utilities.templatetags.builtins.filters import render_markdown
|
|||||||
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
|
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
|
from .constants import LOG_LEVEL_RANK
|
||||||
from .models import *
|
from .models import *
|
||||||
from .scripts import run_script
|
from .scripts import run_script
|
||||||
from .tables import ReportResultsTable, ScriptResultsTable
|
from .tables import ReportResultsTable, ScriptResultsTable
|
||||||
@ -1119,12 +1121,17 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
tests = None
|
tests = None
|
||||||
table = None
|
table = None
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
|
log_threshold = LOG_LEVEL_RANK.get(request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT))
|
||||||
if job.data:
|
if job.data:
|
||||||
|
|
||||||
if 'log' in job.data:
|
if 'log' in job.data:
|
||||||
if 'tests' in job.data:
|
if 'tests' in job.data:
|
||||||
tests = job.data['tests']
|
tests = job.data['tests']
|
||||||
|
|
||||||
for log in job.data['log']:
|
for log in job.data['log']:
|
||||||
|
log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_DEFAULT)
|
||||||
|
if log_level >= log_threshold:
|
||||||
index += 1
|
index += 1
|
||||||
result = {
|
result = {
|
||||||
'index': index,
|
'index': index,
|
||||||
@ -1146,6 +1153,8 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
for method, test_data in tests.items():
|
for method, test_data in tests.items():
|
||||||
if 'log' in test_data:
|
if 'log' in test_data:
|
||||||
for time, status, obj, url, message in test_data['log']:
|
for time, status, obj, url, message in test_data['log']:
|
||||||
|
log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_DEFAULT)
|
||||||
|
if log_level >= log_threshold:
|
||||||
index += 1
|
index += 1
|
||||||
result = {
|
result = {
|
||||||
'index': index,
|
'index': index,
|
||||||
@ -1174,6 +1183,8 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
'script': job.object,
|
'script': job.object,
|
||||||
'job': job,
|
'job': job,
|
||||||
'table': table,
|
'table': table,
|
||||||
|
'log_levels': dict(LogLevelChoices),
|
||||||
|
'log_threshold': request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
if job.data and 'log' in job.data:
|
if job.data and 'log' in job.data:
|
||||||
@ -1200,7 +1211,7 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
# Markdown
|
# Markdown
|
||||||
#
|
#
|
||||||
|
|
||||||
class RenderMarkdownView(View):
|
class RenderMarkdownView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
form = forms.RenderMarkdownForm(request.POST)
|
form = forms.RenderMarkdownForm(request.POST)
|
||||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from circuits.models import Provider
|
from circuits.models import Provider
|
||||||
from dcim.filtersets import InterfaceFilterSet
|
from dcim.filtersets import InterfaceFilterSet
|
||||||
|
from dcim.forms import InterfaceFilterForm
|
||||||
from dcim.models import Interface, Site
|
from dcim.models import Interface, Site
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from tenancy.views import ObjectContactsView
|
from tenancy.views import ObjectContactsView
|
||||||
@ -14,6 +15,7 @@ from utilities.query import count_related
|
|||||||
from utilities.tables import get_table_ordering
|
from utilities.tables import get_table_ordering
|
||||||
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
|
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
|
||||||
from virtualization.filtersets import VMInterfaceFilterSet
|
from virtualization.filtersets import VMInterfaceFilterSet
|
||||||
|
from virtualization.forms import VMInterfaceFilterForm
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import PrefixStatusChoices
|
from .choices import PrefixStatusChoices
|
||||||
@ -206,6 +208,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
|
|||||||
child_model = ASN
|
child_model = ASN
|
||||||
table = tables.ASNTable
|
table = tables.ASNTable
|
||||||
filterset = filtersets.ASNFilterSet
|
filterset = filtersets.ASNFilterSet
|
||||||
|
filterset_form = forms.ASNFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('ASNs'),
|
label=_('ASNs'),
|
||||||
badge=lambda x: x.get_child_asns().count(),
|
badge=lambda x: x.get_child_asns().count(),
|
||||||
@ -337,6 +340,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
|
|||||||
child_model = Prefix
|
child_model = Prefix
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
filterset = filtersets.PrefixFilterSet
|
filterset = filtersets.PrefixFilterSet
|
||||||
|
filterset_form = forms.PrefixFilterForm
|
||||||
template_name = 'ipam/aggregate/prefixes.html'
|
template_name = 'ipam/aggregate/prefixes.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Prefixes'),
|
label=_('Prefixes'),
|
||||||
@ -523,6 +527,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
|
|||||||
child_model = Prefix
|
child_model = Prefix
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
filterset = filtersets.PrefixFilterSet
|
filterset = filtersets.PrefixFilterSet
|
||||||
|
filterset_form = forms.PrefixFilterForm
|
||||||
template_name = 'ipam/prefix/prefixes.html'
|
template_name = 'ipam/prefix/prefixes.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Child Prefixes'),
|
label=_('Child Prefixes'),
|
||||||
@ -558,6 +563,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
|||||||
child_model = IPRange
|
child_model = IPRange
|
||||||
table = tables.IPRangeTable
|
table = tables.IPRangeTable
|
||||||
filterset = filtersets.IPRangeFilterSet
|
filterset = filtersets.IPRangeFilterSet
|
||||||
|
filterset_form = forms.IPRangeFilterForm
|
||||||
template_name = 'ipam/prefix/ip_ranges.html'
|
template_name = 'ipam/prefix/ip_ranges.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Child Ranges'),
|
label=_('Child Ranges'),
|
||||||
@ -584,6 +590,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
|||||||
child_model = IPAddress
|
child_model = IPAddress
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
|
filterset_form = forms.IPAddressFilterForm
|
||||||
template_name = 'ipam/prefix/ip_addresses.html'
|
template_name = 'ipam/prefix/ip_addresses.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('IP Addresses'),
|
label=_('IP Addresses'),
|
||||||
@ -683,6 +690,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
|
|||||||
child_model = IPAddress
|
child_model = IPAddress
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
|
filterset_form = forms.IPRangeFilterForm
|
||||||
template_name = 'ipam/iprange/ip_addresses.html'
|
template_name = 'ipam/iprange/ip_addresses.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('IP Addresses'),
|
label=_('IP Addresses'),
|
||||||
@ -885,6 +893,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
|
|||||||
child_model = IPAddress
|
child_model = IPAddress
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
|
filterset_form = forms.IPAddressFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Related IPs'),
|
label=_('Related IPs'),
|
||||||
badge=lambda x: x.get_related_ips().count(),
|
badge=lambda x: x.get_related_ips().count(),
|
||||||
@ -957,6 +966,7 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
|
|||||||
child_model = VLAN
|
child_model = VLAN
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
filterset = filtersets.VLANFilterSet
|
filterset = filtersets.VLANFilterSet
|
||||||
|
filterset_form = forms.VLANFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('VLANs'),
|
label=_('VLANs'),
|
||||||
badge=lambda x: x.get_child_vlans().count(),
|
badge=lambda x: x.get_child_vlans().count(),
|
||||||
@ -1112,6 +1122,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
|
|||||||
child_model = Interface
|
child_model = Interface
|
||||||
table = tables.VLANDevicesTable
|
table = tables.VLANDevicesTable
|
||||||
filterset = InterfaceFilterSet
|
filterset = InterfaceFilterSet
|
||||||
|
filterset_form = InterfaceFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Device Interfaces'),
|
label=_('Device Interfaces'),
|
||||||
badge=lambda x: x.get_interfaces().count(),
|
badge=lambda x: x.get_interfaces().count(),
|
||||||
@ -1129,6 +1140,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
|
|||||||
child_model = VMInterface
|
child_model = VMInterface
|
||||||
table = tables.VLANVirtualMachinesTable
|
table = tables.VLANVirtualMachinesTable
|
||||||
filterset = VMInterfaceFilterSet
|
filterset = VMInterfaceFilterSet
|
||||||
|
filterset_form = VMInterfaceFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('VM Interfaces'),
|
label=_('VM Interfaces'),
|
||||||
badge=lambda x: x.get_vminterfaces().count(),
|
badge=lambda x: x.get_vminterfaces().count(),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.search import LookupTypes
|
from netbox.search import LookupTypes
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
@ -36,7 +36,8 @@ class SearchForm(forms.Form):
|
|||||||
lookup = forms.ChoiceField(
|
lookup = forms.ChoiceField(
|
||||||
choices=LOOKUP_CHOICES,
|
choices=LOOKUP_CHOICES,
|
||||||
initial=LookupTypes.PARTIAL,
|
initial=LookupTypes.PARTIAL,
|
||||||
required=False
|
required=False,
|
||||||
|
label=_('Lookup')
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from urllib import parse
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth, messages
|
from django.contrib import auth, messages
|
||||||
@ -33,20 +32,15 @@ class CoreMiddleware:
|
|||||||
# Assign a random unique ID to the request. This will be used for change logging.
|
# Assign a random unique ID to the request. This will be used for change logging.
|
||||||
request.id = uuid.uuid4()
|
request.id = uuid.uuid4()
|
||||||
|
|
||||||
# Enforce the LOGIN_REQUIRED config parameter. If true, redirect all non-exempt unauthenticated requests
|
|
||||||
# to the login page.
|
|
||||||
if (
|
|
||||||
settings.LOGIN_REQUIRED and
|
|
||||||
not request.user.is_authenticated and
|
|
||||||
not request.path_info.startswith(settings.AUTH_EXEMPT_PATHS)
|
|
||||||
):
|
|
||||||
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
|
|
||||||
return HttpResponseRedirect(login_url)
|
|
||||||
|
|
||||||
# Enable the event_tracking context manager and process the request.
|
# Enable the event_tracking context manager and process the request.
|
||||||
with event_tracking(request):
|
with event_tracking(request):
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Check if language cookie should be renewed
|
||||||
|
if request.user.is_authenticated and settings.SESSION_SAVE_EVERY_REQUEST:
|
||||||
|
if language := request.user.config.get('locale.language'):
|
||||||
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||||
|
|
||||||
# Attach the unique request ID as an HTTP header.
|
# Attach the unique request ID as an HTTP header.
|
||||||
response['X-Request-ID'] = request.id
|
response['X-Request-ID'] = request.id
|
||||||
|
|
||||||
|
@ -468,16 +468,13 @@ MENUS = [
|
|||||||
PROVISIONING_MENU,
|
PROVISIONING_MENU,
|
||||||
CUSTOMIZATION_MENU,
|
CUSTOMIZATION_MENU,
|
||||||
OPERATIONS_MENU,
|
OPERATIONS_MENU,
|
||||||
ADMIN_MENU,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
#
|
# Add top-level plugin menus
|
||||||
# Add plugin menus
|
|
||||||
#
|
|
||||||
|
|
||||||
for menu in registry['plugins']['menus']:
|
for menu in registry['plugins']['menus']:
|
||||||
MENUS.append(menu)
|
MENUS.append(menu)
|
||||||
|
|
||||||
|
# Add the default "plugins" menu
|
||||||
if registry['plugins']['menu_items']:
|
if registry['plugins']['menu_items']:
|
||||||
|
|
||||||
# Build the default plugins menu
|
# Build the default plugins menu
|
||||||
@ -491,3 +488,6 @@ if registry['plugins']['menu_items']:
|
|||||||
groups=groups
|
groups=groups
|
||||||
)
|
)
|
||||||
MENUS.append(plugins_menu)
|
MENUS.append(plugins_menu)
|
||||||
|
|
||||||
|
# Add the admin menu last
|
||||||
|
MENUS.append(ADMIN_MENU)
|
||||||
|
@ -18,8 +18,8 @@ def register_template_extensions(class_list):
|
|||||||
"""
|
"""
|
||||||
Register a list of PluginTemplateExtension classes
|
Register a list of PluginTemplateExtension classes
|
||||||
"""
|
"""
|
||||||
# Validation
|
|
||||||
for template_extension in class_list:
|
for template_extension in class_list:
|
||||||
|
# Validation
|
||||||
if not inspect.isclass(template_extension):
|
if not inspect.isclass(template_extension):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
_("PluginTemplateExtension class {template_extension} was passed as an instance!").format(
|
_("PluginTemplateExtension class {template_extension} was passed as an instance!").format(
|
||||||
@ -33,7 +33,17 @@ def register_template_extensions(class_list):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
if template_extension.models:
|
||||||
|
# Registration for multiple models
|
||||||
|
models = template_extension.models
|
||||||
|
elif template_extension.model:
|
||||||
|
# Registration for a single model
|
||||||
|
models = [template_extension.model]
|
||||||
|
else:
|
||||||
|
# Global registration (no specific models)
|
||||||
|
models = [None]
|
||||||
|
for model in models:
|
||||||
|
registry['plugins']['template_extensions'][model].append(template_extension)
|
||||||
|
|
||||||
|
|
||||||
def register_menu(menu):
|
def register_menu(menu):
|
||||||
|
@ -20,6 +20,7 @@ class PluginTemplateExtension:
|
|||||||
* settings - Global NetBox settings
|
* settings - Global NetBox settings
|
||||||
* config - Plugin-specific configuration parameters
|
* config - Plugin-specific configuration parameters
|
||||||
"""
|
"""
|
||||||
|
models = None
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
|
@ -8,6 +8,7 @@ from django.db.models.fields.related import ForeignKey
|
|||||||
from django.db.models.functions import window
|
from django.db.models.functions import window
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
import netaddr
|
import netaddr
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ class SearchBackend:
|
|||||||
# Organize choices by category
|
# Organize choices by category
|
||||||
categories = defaultdict(dict)
|
categories = defaultdict(dict)
|
||||||
for label, idx in registry['search'].items():
|
for label, idx in registry['search'].items():
|
||||||
categories[idx.get_category()][label] = title(idx.model._meta.verbose_name)
|
categories[idx.get_category()][label] = _(title(idx.model._meta.verbose_name))
|
||||||
|
|
||||||
# Compile a nested tuple of choices for form rendering
|
# Compile a nested tuple of choices for form rendering
|
||||||
results = (
|
results = (
|
||||||
|
@ -149,6 +149,7 @@ SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
|||||||
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
|
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
|
||||||
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||||
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
||||||
|
SENTRY_SEND_DEFAULT_PII = getattr(configuration, 'SENTRY_SEND_DEFAULT_PII', False)
|
||||||
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
||||||
SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
|
SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
|
||||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||||
@ -227,6 +228,23 @@ if STORAGE_BACKEND is not None:
|
|||||||
return globals().get(name, default)
|
return globals().get(name, default)
|
||||||
storages.utils.setting = _setting
|
storages.utils.setting = _setting
|
||||||
|
|
||||||
|
# django-storage-swift
|
||||||
|
elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
||||||
|
try:
|
||||||
|
import swift.utils # type: ignore
|
||||||
|
except ModuleNotFoundError as e:
|
||||||
|
if getattr(e, 'name') == 'swift':
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
|
||||||
|
"It can be installed by running 'pip install django-storage-swift'."
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Load all SWIFT_* settings from the user configuration
|
||||||
|
for param, value in STORAGE_CONFIG.items():
|
||||||
|
if param.startswith('SWIFT_'):
|
||||||
|
globals()[param] = value
|
||||||
|
|
||||||
if STORAGE_CONFIG and STORAGE_BACKEND is None:
|
if STORAGE_CONFIG and STORAGE_BACKEND is None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
|
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
|
||||||
@ -502,15 +520,6 @@ EXEMPT_EXCLUDE_MODELS = (
|
|||||||
('users', 'user'),
|
('users', 'user'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# All URLs starting with a string listed here are exempt from login enforcement
|
|
||||||
AUTH_EXEMPT_PATHS = (
|
|
||||||
f'/{BASE_PATH}api/',
|
|
||||||
f'/{BASE_PATH}graphql/',
|
|
||||||
f'/{BASE_PATH}login/',
|
|
||||||
f'/{BASE_PATH}oauth/',
|
|
||||||
f'/{BASE_PATH}metrics',
|
|
||||||
)
|
|
||||||
|
|
||||||
# All URLs starting with a string listed here are exempt from maintenance mode enforcement
|
# All URLs starting with a string listed here are exempt from maintenance mode enforcement
|
||||||
MAINTENANCE_EXEMPT_PATHS = (
|
MAINTENANCE_EXEMPT_PATHS = (
|
||||||
f'/{BASE_PATH}admin/',
|
f'/{BASE_PATH}admin/',
|
||||||
@ -538,7 +547,7 @@ if SENTRY_ENABLED:
|
|||||||
release=RELEASE.full_version,
|
release=RELEASE.full_version,
|
||||||
sample_rate=SENTRY_SAMPLE_RATE,
|
sample_rate=SENTRY_SAMPLE_RATE,
|
||||||
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
||||||
send_default_pii=True,
|
send_default_pii=SENTRY_SEND_DEFAULT_PII,
|
||||||
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
||||||
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
|
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ class GlobalContent(PluginTemplateExtension):
|
|||||||
|
|
||||||
|
|
||||||
class SiteContent(PluginTemplateExtension):
|
class SiteContent(PluginTemplateExtension):
|
||||||
model = 'dcim.site'
|
models = ['dcim.site']
|
||||||
|
|
||||||
def left_page(self):
|
def left_page(self):
|
||||||
return "SITE CONTENT - LEFT PAGE"
|
return "SITE CONTENT - LEFT PAGE"
|
||||||
|
@ -176,7 +176,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
'model': model,
|
'model': model,
|
||||||
'table': table,
|
'table': table,
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
'filter_form': self.filterset_form(request.GET) if self.filterset_form else None,
|
||||||
'prerequisite_model': get_prerequisite_model(self.queryset),
|
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||||
**self.get_extra_context(request),
|
**self.get_extra_context(request),
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@ -12,7 +13,7 @@ from extras.forms import JournalEntryForm
|
|||||||
from extras.models import JournalEntry
|
from extras.models import JournalEntry
|
||||||
from extras.tables import JournalEntryTable
|
from extras.tables import JournalEntryTable
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.views import GetReturnURLMixin, ViewTab
|
from utilities.views import ConditionalLoginRequiredMixin, GetReturnURLMixin, ViewTab
|
||||||
from .base import BaseMultiObjectView
|
from .base import BaseMultiObjectView
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -24,7 +25,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeLogView(View):
|
class ObjectChangeLogView(ConditionalLoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Present a history of changes made to a particular object. The model class must be passed as a keyword argument
|
Present a history of changes made to a particular object. The model class must be passed as a keyword argument
|
||||||
when referencing this view in a URL path. For example:
|
when referencing this view in a URL path. For example:
|
||||||
@ -77,7 +78,7 @@ class ObjectChangeLogView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectJournalView(View):
|
class ObjectJournalView(ConditionalLoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this
|
Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this
|
||||||
view in a URL path. For example:
|
view in a URL path. For example:
|
||||||
@ -138,7 +139,7 @@ class ObjectJournalView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectJobsView(View):
|
class ObjectJobsView(ConditionalLoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Render a list of all Job assigned to an object. For example:
|
Render a list of all Job assigned to an object. For example:
|
||||||
|
|
||||||
@ -191,7 +192,7 @@ class ObjectJobsView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectSyncDataView(View):
|
class ObjectSyncDataView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
def post(self, request, model, **kwargs):
|
def post(self, request, model, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -87,12 +87,14 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
child_model: The model class which represents the child objects
|
child_model: The model class which represents the child objects
|
||||||
table: The django-tables2 Table class used to render the child objects list
|
table: The django-tables2 Table class used to render the child objects list
|
||||||
filterset: A django-filter FilterSet that is applied to the queryset
|
filterset: A django-filter FilterSet that is applied to the queryset
|
||||||
|
filterset_form: The form class used to render filter options
|
||||||
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
||||||
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
||||||
"""
|
"""
|
||||||
child_model = None
|
child_model = None
|
||||||
table = None
|
table = None
|
||||||
filterset = None
|
filterset = None
|
||||||
|
filterset_form = None
|
||||||
template_name = 'generic/object_children.html'
|
template_name = 'generic/object_children.html'
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@ -152,6 +154,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
|
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
|
||||||
'table': table,
|
'table': table,
|
||||||
'table_config': f'{table.name}_config',
|
'table_config': f'{table.name}_config',
|
||||||
|
'filter_form': self.filterset_form(request.GET) if self.filterset_form else None,
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
'tab': self.tab,
|
'tab': self.tab,
|
||||||
'return_url': request.get_full_path(),
|
'return_url': request.get_full_path(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
@ -6,7 +7,7 @@ from django.utils.module_loading import import_string
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
|
|
||||||
class ObjectSelectorView(View):
|
class ObjectSelectorView(LoginRequiredMixin, View):
|
||||||
template_name = 'htmx/object_selector.html'
|
template_name = 'htmx/object_selector.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
@ -19,6 +19,7 @@ from netbox.search.backends import search_backend
|
|||||||
from netbox.tables import SearchTable
|
from netbox.tables import SearchTable
|
||||||
from utilities.htmx import htmx_partial
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
from utilities.views import ConditionalLoginRequiredMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'HomeView',
|
'HomeView',
|
||||||
@ -28,7 +29,7 @@ __all__ = (
|
|||||||
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
||||||
|
|
||||||
|
|
||||||
class HomeView(View):
|
class HomeView(ConditionalLoginRequiredMixin, View):
|
||||||
template_name = 'home.html'
|
template_name = 'home.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@ -62,7 +63,7 @@ class HomeView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SearchView(View):
|
class SearchView(ConditionalLoginRequiredMixin, View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
results = []
|
results = []
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -27,7 +27,7 @@
|
|||||||
"bootstrap": "5.3.3",
|
"bootstrap": "5.3.3",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"gridstack": "10.2.1",
|
"gridstack": "10.3.0",
|
||||||
"htmx.org": "1.9.12",
|
"htmx.org": "1.9.12",
|
||||||
"query-string": "9.0.0",
|
"query-string": "9.0.0",
|
||||||
"sass": "1.77.6",
|
"sass": "1.77.6",
|
||||||
|
@ -74,20 +74,25 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
|
|
||||||
load(value: string) {
|
load(value: string) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const url = self.getRequestUrl(value);
|
|
||||||
|
|
||||||
// Automatically clear any cached options. (Only options included
|
// Automatically clear any cached options. (Only options included
|
||||||
// in the API response should be present.)
|
// in the API response should be present.)
|
||||||
self.clearOptions();
|
self.clearOptions();
|
||||||
|
|
||||||
addClasses(self.wrapper, self.settings.loadingClass);
|
|
||||||
self.loading++;
|
|
||||||
|
|
||||||
// Populate the null option (if any) if not searching
|
// Populate the null option (if any) if not searching
|
||||||
if (self.nullOption && !value) {
|
if (self.nullOption && !value) {
|
||||||
self.addOption(self.nullOption);
|
self.addOption(self.nullOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the API request URL. If none is provided, abort as no request can be made.
|
||||||
|
const url = self.getRequestUrl(value);
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClasses(self.wrapper, self.settings.loadingClass);
|
||||||
|
self.loading++;
|
||||||
|
|
||||||
// Make the API request
|
// Make the API request
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@ -129,6 +134,9 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
for (const result of this.api_url.matchAll(new RegExp(`({{${key}}})`, 'g'))) {
|
for (const result of this.api_url.matchAll(new RegExp(`({{${key}}})`, 'g'))) {
|
||||||
if (value) {
|
if (value) {
|
||||||
url = replaceAll(url, result[1], value.toString());
|
url = replaceAll(url, result[1], value.toString());
|
||||||
|
} else {
|
||||||
|
// No value is available to replace the token; abort.
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1754,10 +1754,10 @@ graphql@16.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||||
|
|
||||||
gridstack@10.2.1:
|
gridstack@10.3.0:
|
||||||
version "10.2.1"
|
version "10.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.1.tgz#3ce6119ae86cfb0a533c5f0d15b03777a55384ca"
|
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.0.tgz#8fa065f896d0a880c5c54c24d189f3197184488a"
|
||||||
integrity sha512-UAPKnIvd9sIqPDFMtKMqj0G5GDj8MUFPcelRJq7FzQFSxSYBblKts/Gd52iEJg0EvTFP51t6ZuMWGx0pSSFBdw==
|
integrity sha512-eGKsmU2TppV4coyDu9IIdIkm4qjgLLdjlEOFwQyQMuSwfOpzSfLdPc8du0HuebGr7CvAIrJxN4lBOmGrWSBg9g==
|
||||||
|
|
||||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Current Configuration" %}</h5>
|
<h5 class="card-header">{% trans "Current Configuration" %}</h5>
|
||||||
{% include 'core/inc/config_data.html' with config=config.data %}
|
{% include 'core/inc/config_data.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,28 +125,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<thead>
|
||||||
|
<tr class="border-bottom">
|
||||||
<th>{% trans "Device" %}</th>
|
<th>{% trans "Device" %}</th>
|
||||||
<th>{% trans "Position" %}</th>
|
<th>{% trans "Position" %}</th>
|
||||||
<th>{% trans "Master" %}</th>
|
<th>{% trans "Master" %}</th>
|
||||||
<th>{% trans "Priority" %}</th>
|
<th>{% trans "Priority" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for vc_member in vc_members %}
|
{% for vc_member in vc_members %}
|
||||||
<tr{% if vc_member == object %} class="info"{% endif %}>
|
<tr{% if vc_member == object %} class="table-primary"{% endif %}>
|
||||||
|
<td>{{ vc_member|linkify }}</td>
|
||||||
|
<td>{% badge vc_member.vc_position show_empty=True %}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ vc_member|linkify }}
|
{% if object.virtual_chassis.master == vc_member %}
|
||||||
</td>
|
{% checkmark True %}
|
||||||
<td>
|
{% else %}
|
||||||
{% badge vc_member.vc_position show_empty=True %}
|
{{ ''|placeholder }}
|
||||||
</td>
|
{% endif %}
|
||||||
<td>
|
|
||||||
{% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ vc_member.vc_priority|placeholder }}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ vc_member.vc_priority|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -221,6 +223,11 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if object.oob_ip %}
|
{% if object.oob_ip %}
|
||||||
<a href="{{ object.oob_ip.get_absolute_url }}" id="oob_ip">{{ object.oob_ip.address.ip }}</a>
|
<a href="{{ object.oob_ip.get_absolute_url }}" id="oob_ip">{{ object.oob_ip.address.ip }}</a>
|
||||||
|
{% if object.oob_ip.nat_inside %}
|
||||||
|
({% trans "NAT for" %} <a href="{{ object.oob_ip.nat_inside.get_absolute_url }}">{{ object.oob_ip.nat_inside.address.ip }}</a>)
|
||||||
|
{% elif object.oob_ip.nat_outside.exists %}
|
||||||
|
({% trans "NAT" %}: {% for nat in object.oob_ip.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
|
{% endif %}
|
||||||
{% copy_content "oob_ip" %}
|
{% copy_content "oob_ip" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
|
@ -42,8 +42,26 @@
|
|||||||
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
|
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
|
||||||
|
|
||||||
{# Object table controls #}
|
{# Object table controls #}
|
||||||
<div class="row mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="col-auto ms-auto d-print-none">
|
<div>{% trans "Log threshold" %}</div>
|
||||||
|
|
||||||
|
<div class="px-2 d-print-none">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ log_levels|get_key:log_threshold }}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
{% for level, name in log_levels.items %}
|
||||||
|
<a class="dropdown-item d-flex justify-content-between" href="{% url 'extras:script_result' job_pk=job.pk %}?log_threshold={{ level }}">
|
||||||
|
{{ name }}
|
||||||
|
{% if level == log_threshold %}<span class="badge bg-green ms-auto"></span>{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ms-auto d-print-none">
|
||||||
{% if request.user.is_authenticated and job.completed %}
|
{% if request.user.is_authenticated and job.completed %}
|
||||||
<div class="table-configure input-group">
|
<div class="table-configure input-group">
|
||||||
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"
|
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"
|
||||||
|
@ -48,7 +48,7 @@ Context:
|
|||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
<a class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||||
{% trans "Results" %}
|
{% trans "Results" %}
|
||||||
<span class="badge text-bg-secondary total-object-count">{% if table.page.paginator.count %}{{ table.page.paginator.count }}{% else %}{{ total_count }}{% endif %}</span>
|
<span class="badge text-bg-secondary total-object-count">{% if table.page.paginator.count %}{{ table.page.paginator.count }}{% else %}{{ total_count|default:"0" }}{% endif %}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if filter_form %}
|
||||||
<div class="col-auto d-print-none">
|
<div class="col-auto d-print-none">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
@ -21,6 +22,7 @@
|
|||||||
{{ filter_form.filter_id }}
|
{{ filter_form.filter_id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-auto ms-auto d-print-none">
|
<div class="col-auto ms-auto d-print-none">
|
||||||
{% if request.user.is_authenticated and table_modal %}
|
{% if request.user.is_authenticated and table_modal %}
|
||||||
|
@ -53,10 +53,6 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
{% render_field form.comments %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -65,4 +61,8 @@
|
|||||||
{% render_custom_fields form %}
|
{% render_custom_fields form %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
{% render_field form.comments %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,6 +13,7 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
|||||||
child_model = ContactAssignment
|
child_model = ContactAssignment
|
||||||
table = tables.ContactAssignmentTable
|
table = tables.ContactAssignmentTable
|
||||||
filterset = filtersets.ContactAssignmentFilterSet
|
filterset = filtersets.ContactAssignmentFilterSet
|
||||||
|
filterset_form = forms.ContactAssignmentFilterForm
|
||||||
template_name = 'tenancy/object_contacts.html'
|
template_name = 'tenancy/object_contacts.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Contacts'),
|
label=_('Contacts'),
|
||||||
@ -364,7 +365,7 @@ class ContactAssignmentEditView(generic.ObjectEditView):
|
|||||||
|
|
||||||
def get_extra_addanother_params(self, request):
|
def get_extra_addanother_params(self, request):
|
||||||
return {
|
return {
|
||||||
'content_type': request.GET.get('content_type'),
|
'object_type': request.GET.get('object_type'),
|
||||||
'object_id': request.GET.get('object_id'),
|
'object_id': request.GET.get('object_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from utilities.ordering import naturalize
|
from utilities.ordering import naturalize
|
||||||
@ -26,6 +27,7 @@ class ColorField(models.CharField):
|
|||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
kwargs['widget'] = ColorSelect
|
kwargs['widget'] = ColorSelect
|
||||||
|
kwargs['help_text'] = mark_safe(_('RGB color in hexadecimal. Example: ') + '<code>00ff00</code>')
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ def prepare_cloned_fields(instance):
|
|||||||
for key, value in attrs.items():
|
for key, value in attrs.items():
|
||||||
if type(value) in (list, tuple):
|
if type(value) in (list, tuple):
|
||||||
params.extend([(key, v) for v in value])
|
params.extend([(key, v) for v in value])
|
||||||
elif value not in (False, None):
|
elif value is not False and value is not None:
|
||||||
params.append((key, value))
|
params.append((key, value))
|
||||||
else:
|
else:
|
||||||
params.append((key, ''))
|
params.append((key, ''))
|
||||||
|
@ -22,8 +22,10 @@ def _get_registered_content(obj, method, template_context):
|
|||||||
'perms': template_context['perms'],
|
'perms': template_context['perms'],
|
||||||
}
|
}
|
||||||
|
|
||||||
model_name = obj._meta.label_lower if obj is not None else None
|
template_extensions = list(registry['plugins']['template_extensions'].get(None, []))
|
||||||
template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
|
if obj is not None:
|
||||||
|
model_name = obj._meta.label_lower
|
||||||
|
template_extensions.extend(registry['plugins']['template_extensions'].get(model_name, []))
|
||||||
for template_extension in template_extensions:
|
for template_extension in template_extensions:
|
||||||
|
|
||||||
# If the class has not overridden the specified method, we can skip it (because we know it
|
# If the class has not overridden the specified method, we can skip it (because we know it
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import AccessMixin
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -13,6 +14,7 @@ from utilities.relations import get_related_models
|
|||||||
from .permissions import resolve_permission
|
from .permissions import resolve_permission
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ConditionalLoginRequiredMixin',
|
||||||
'ContentTypePermissionRequiredMixin',
|
'ContentTypePermissionRequiredMixin',
|
||||||
'GetRelatedModelsMixin',
|
'GetRelatedModelsMixin',
|
||||||
'GetReturnURLMixin',
|
'GetReturnURLMixin',
|
||||||
@ -27,10 +29,20 @@ __all__ = (
|
|||||||
# View Mixins
|
# View Mixins
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContentTypePermissionRequiredMixin(AccessMixin):
|
class ConditionalLoginRequiredMixin(AccessMixin):
|
||||||
|
"""
|
||||||
|
Similar to Django's LoginRequiredMixin, but enforces authentication only if LOGIN_REQUIRED is True.
|
||||||
|
"""
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypePermissionRequiredMixin(ConditionalLoginRequiredMixin):
|
||||||
"""
|
"""
|
||||||
Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments.
|
Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments.
|
||||||
This is related to ObjectPermissionRequiredMixin, except that is does not enforce object-level permissions,
|
This is related to ObjectPermissionRequiredMixin, except that it does not enforce object-level permissions,
|
||||||
and fits within NetBox's custom permission enforcement system.
|
and fits within NetBox's custom permission enforcement system.
|
||||||
|
|
||||||
additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
|
additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
|
||||||
@ -63,7 +75,7 @@ class ContentTypePermissionRequiredMixin(AccessMixin):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermissionRequiredMixin(AccessMixin):
|
class ObjectPermissionRequiredMixin(ConditionalLoginRequiredMixin):
|
||||||
"""
|
"""
|
||||||
Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level
|
Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level
|
||||||
permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered
|
permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered
|
||||||
|
@ -184,8 +184,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
|||||||
'cluster': _('A virtual machine must be assigned to a site and/or cluster.')
|
'cluster': _('A virtual machine must be assigned to a site and/or cluster.')
|
||||||
})
|
})
|
||||||
|
|
||||||
# Validate site for cluster & device
|
# Validate site for cluster & VM
|
||||||
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
|
if self.cluster and self.site and self.cluster.site and self.cluster.site != self.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'cluster': _(
|
'cluster': _(
|
||||||
'The selected cluster ({cluster}) is not assigned to this site ({site}).'
|
'The selected cluster ({cluster}) is not assigned to this site ({site}).'
|
||||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext as _
|
|||||||
from jinja2.exceptions import TemplateError
|
from jinja2.exceptions import TemplateError
|
||||||
|
|
||||||
from dcim.filtersets import DeviceFilterSet
|
from dcim.filtersets import DeviceFilterSet
|
||||||
|
from dcim.forms import DeviceFilterForm
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
@ -173,6 +174,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
|
|||||||
child_model = VirtualMachine
|
child_model = VirtualMachine
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
filterset = filtersets.VirtualMachineFilterSet
|
filterset = filtersets.VirtualMachineFilterSet
|
||||||
|
filterset_form = forms.VirtualMachineFilterForm
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Virtual Machines'),
|
label=_('Virtual Machines'),
|
||||||
badge=lambda obj: obj.virtual_machines.count(),
|
badge=lambda obj: obj.virtual_machines.count(),
|
||||||
@ -190,6 +192,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
|
|||||||
child_model = Device
|
child_model = Device
|
||||||
table = DeviceTable
|
table = DeviceTable
|
||||||
filterset = DeviceFilterSet
|
filterset = DeviceFilterSet
|
||||||
|
filterset_form = DeviceFilterForm
|
||||||
template_name = 'virtualization/cluster/devices.html'
|
template_name = 'virtualization/cluster/devices.html'
|
||||||
actions = {
|
actions = {
|
||||||
'add': {'add'},
|
'add': {'add'},
|
||||||
@ -350,6 +353,7 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
|
|||||||
child_model = VMInterface
|
child_model = VMInterface
|
||||||
table = tables.VirtualMachineVMInterfaceTable
|
table = tables.VirtualMachineVMInterfaceTable
|
||||||
filterset = filtersets.VMInterfaceFilterSet
|
filterset = filtersets.VMInterfaceFilterSet
|
||||||
|
filterset_form = forms.VMInterfaceFilterForm
|
||||||
template_name = 'virtualization/virtualmachine/interfaces.html'
|
template_name = 'virtualization/virtualmachine/interfaces.html'
|
||||||
actions = {
|
actions = {
|
||||||
**DEFAULT_ACTION_PERMISSIONS,
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
@ -375,6 +379,7 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
|
|||||||
child_model = VirtualDisk
|
child_model = VirtualDisk
|
||||||
table = tables.VirtualMachineVirtualDiskTable
|
table = tables.VirtualMachineVirtualDiskTable
|
||||||
filterset = filtersets.VirtualDiskFilterSet
|
filterset = filtersets.VirtualDiskFilterSet
|
||||||
|
filterset_form = forms.VirtualDiskFilterForm
|
||||||
template_name = 'virtualization/virtualmachine/virtual_disks.html'
|
template_name = 'virtualization/virtualmachine/virtual_disks.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Virtual Disks'),
|
label=_('Virtual Disks'),
|
||||||
|
@ -22,7 +22,8 @@ class IKEProposalSerializer(NetBoxModelSerializer):
|
|||||||
choices=EncryptionAlgorithmChoices
|
choices=EncryptionAlgorithmChoices
|
||||||
)
|
)
|
||||||
authentication_algorithm = ChoiceField(
|
authentication_algorithm = ChoiceField(
|
||||||
choices=AuthenticationAlgorithmChoices
|
choices=AuthenticationAlgorithmChoices,
|
||||||
|
required=False
|
||||||
)
|
)
|
||||||
group = ChoiceField(
|
group = ChoiceField(
|
||||||
choices=DHGroupChoices
|
choices=DHGroupChoices
|
||||||
@ -43,7 +44,8 @@ class IKEPolicySerializer(NetBoxModelSerializer):
|
|||||||
choices=IKEVersionChoices
|
choices=IKEVersionChoices
|
||||||
)
|
)
|
||||||
mode = ChoiceField(
|
mode = ChoiceField(
|
||||||
choices=IKEModeChoices
|
choices=IKEModeChoices,
|
||||||
|
required=False
|
||||||
)
|
)
|
||||||
proposals = SerializedPKRelatedField(
|
proposals = SerializedPKRelatedField(
|
||||||
queryset=IKEProposal.objects.all(),
|
queryset=IKEProposal.objects.all(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==5.0.6
|
Django==5.0.7
|
||||||
django-cors-headers==4.4.0
|
django-cors-headers==4.4.0
|
||||||
django-debug-toolbar==4.3.0
|
django-debug-toolbar==4.3.0
|
||||||
django-filter==24.2
|
django-filter==24.2
|
||||||
@ -12,26 +12,26 @@ django-rich==1.9.0
|
|||||||
django-rq==2.10.2
|
django-rq==2.10.2
|
||||||
django-taggit==5.0.1
|
django-taggit==5.0.1
|
||||||
django-tables2==2.7.0
|
django-tables2==2.7.0
|
||||||
django-timezone-field==6.1.0
|
django-timezone-field==7.0
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
drf-spectacular==0.27.2
|
drf-spectacular==0.27.2
|
||||||
drf-spectacular-sidecar==2024.6.1
|
drf-spectacular-sidecar==2024.7.1
|
||||||
feedparser==6.0.11
|
feedparser==6.0.11
|
||||||
gunicorn==22.0.0
|
gunicorn==22.0.0
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.4
|
||||||
Markdown==3.6
|
Markdown==3.6
|
||||||
mkdocs-material==9.5.27
|
mkdocs-material==9.5.28
|
||||||
mkdocstrings[python-legacy]==0.25.1
|
mkdocstrings[python-legacy]==0.25.1
|
||||||
netaddr==1.3.0
|
netaddr==1.3.0
|
||||||
nh3==0.2.17
|
nh3==0.2.18
|
||||||
Pillow==10.3.0
|
Pillow==10.4.0
|
||||||
psycopg[c,pool]==3.1.19
|
psycopg[c,pool]==3.2.1
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
social-auth-app-django==5.4.1
|
social-auth-app-django==5.4.1
|
||||||
social-auth-core==4.5.4
|
social-auth-core==4.5.4
|
||||||
strawberry-graphql==0.235.0
|
strawberry-graphql==0.235.2
|
||||||
strawberry-graphql-django==0.44.2
|
strawberry-graphql-django==0.46.1
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.6.1
|
tablib==3.6.1
|
||||||
tzdata==2024.1
|
tzdata==2024.1
|
||||||
|
@ -33,7 +33,7 @@ echo "Using ${PYTHON_VERSION}"
|
|||||||
|
|
||||||
# Remove the existing virtual environment (if any)
|
# Remove the existing virtual environment (if any)
|
||||||
if [ -d "$VIRTUALENV" ]; then
|
if [ -d "$VIRTUALENV" ]; then
|
||||||
COMMAND="rm -rf ${VIRTUALENV}"
|
COMMAND="rm -rf \"${VIRTUALENV}\""
|
||||||
echo "Removing old virtual environment..."
|
echo "Removing old virtual environment..."
|
||||||
eval $COMMAND
|
eval $COMMAND
|
||||||
else
|
else
|
||||||
@ -41,7 +41,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a new virtual environment
|
# Create a new virtual environment
|
||||||
COMMAND="${PYTHON} -m venv ${VIRTUALENV}"
|
COMMAND="${PYTHON} -m venv \"${VIRTUALENV}\""
|
||||||
echo "Creating a new virtual environment at ${VIRTUALENV}..."
|
echo "Creating a new virtual environment at ${VIRTUALENV}..."
|
||||||
eval $COMMAND || {
|
eval $COMMAND || {
|
||||||
echo "--------------------------------------------------------------------"
|
echo "--------------------------------------------------------------------"
|
||||||
|
Loading…
Reference in New Issue
Block a user