mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
dceffca849
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.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: v3.4.2
|
placeholder: v3.4.3
|
||||||
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: v3.4.2
|
placeholder: v3.4.3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
# NetBox v3.4
|
# NetBox v3.4
|
||||||
|
|
||||||
## v3.4.3 (FUTURE)
|
## v3.4.4 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.4.3 (2023-01-20)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#9996](https://github.com/netbox-community/netbox/issues/9996) - Introduce `CA_CERT_PATH` parameter to define SSL CA path for Redis servers
|
* [#9996](https://github.com/netbox-community/netbox/issues/9996) - Introduce `CA_CERT_PATH` parameter to define SSL CA path for Redis servers
|
||||||
* [#10486](https://github.com/netbox-community/netbox/issues/10486) - Add a cable edit button for connected components in component lists
|
* [#10486](https://github.com/netbox-community/netbox/issues/10486) - Add a cable edit button for connected components in component lists
|
||||||
|
* [#11118](https://github.com/netbox-community/netbox/issues/11118) - Add L2VPN filters for VLANs and interfaces
|
||||||
* [#11150](https://github.com/netbox-community/netbox/issues/11150) - Add primary IPv4/v6 address filters for devices
|
* [#11150](https://github.com/netbox-community/netbox/issues/11150) - Add primary IPv4/v6 address filters for devices
|
||||||
|
* [#11227](https://github.com/netbox-community/netbox/issues/11227) - Add 800GE interface types
|
||||||
|
* [#11228](https://github.com/netbox-community/netbox/issues/11228) - List both devices & VMs under device role view
|
||||||
* [#11245](https://github.com/netbox-community/netbox/issues/11245) - Enable export templates for journal entries
|
* [#11245](https://github.com/netbox-community/netbox/issues/11245) - Enable export templates for journal entries
|
||||||
* [#11371](https://github.com/netbox-community/netbox/issues/11371) - Introduce additional 100M Ethernet interface types
|
* [#11371](https://github.com/netbox-community/netbox/issues/11371) - Introduce additional 100M Ethernet interface types
|
||||||
|
|
||||||
@ -20,10 +27,16 @@
|
|||||||
* [#11402](https://github.com/netbox-community/netbox/issues/11402) - Avoid LookupError exception when running scripts with commit disabled
|
* [#11402](https://github.com/netbox-community/netbox/issues/11402) - Avoid LookupError exception when running scripts with commit disabled
|
||||||
* [#11403](https://github.com/netbox-community/netbox/issues/11403) - Fix exception when scheduling a job in the past
|
* [#11403](https://github.com/netbox-community/netbox/issues/11403) - Fix exception when scheduling a job in the past
|
||||||
* [#11416](https://github.com/netbox-community/netbox/issues/11416) - Avoid AttributeError exception when deleting a cabled circuit termination
|
* [#11416](https://github.com/netbox-community/netbox/issues/11416) - Avoid AttributeError exception when deleting a cabled circuit termination
|
||||||
|
* [#11433](https://github.com/netbox-community/netbox/issues/11433) - Avoid AttributeError exception when generating API schema for views with custom schema
|
||||||
* [#11438](https://github.com/netbox-community/netbox/issues/11438) - Fix deletion of scheduled job using non-default queues
|
* [#11438](https://github.com/netbox-community/netbox/issues/11438) - Fix deletion of scheduled job using non-default queues
|
||||||
* [#11444](https://github.com/netbox-community/netbox/issues/11444) - Adding/removing a device from a device bay should record a pre-change snapshot on the device bay
|
* [#11444](https://github.com/netbox-community/netbox/issues/11444) - Adding/removing a device from a device bay should record a pre-change snapshot on the device bay
|
||||||
* [#11467](https://github.com/netbox-community/netbox/issues/11467) - Correct count on interfaces tab when viewing a VC master device
|
* [#11467](https://github.com/netbox-community/netbox/issues/11467) - Correct count on interfaces tab when viewing a VC master device
|
||||||
* [#11483](https://github.com/netbox-community/netbox/issues/11483) - Apply configured formatting to custom date fields
|
* [#11483](https://github.com/netbox-community/netbox/issues/11483) - Apply configured formatting to custom date fields
|
||||||
|
* [#11488](https://github.com/netbox-community/netbox/issues/11488) - Add missing `description` fields to several REST API serializers
|
||||||
|
* [#11497](https://github.com/netbox-community/netbox/issues/11497) - Enforce `run_script` permission when executing scripts via REST API
|
||||||
|
* [#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex
|
||||||
|
* [#11522](https://github.com/netbox-community/netbox/issues/11522) - Correct tag links under contact & tenant list views
|
||||||
|
* [#11544](https://github.com/netbox-community/netbox/issues/11544) - Catch ValidationError exception when filtering by invalid MAC address
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -77,6 +77,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
|||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||||
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -672,6 +672,22 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
|
config_context = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(DeviceSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
|
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
||||||
|
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
|
||||||
|
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
|
def get_config_context(self, obj):
|
||||||
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
@ -687,7 +703,8 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
|||||||
model = VirtualDeviceContext
|
model = VirtualDeviceContext
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
|
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
|
||||||
'primary_ip6', 'status', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count',
|
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'interface_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -706,22 +723,6 @@ class ModuleSerializer(NetBoxModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|
||||||
config_context = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta(DeviceSerializer.Meta):
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
|
||||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
|
||||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
|
|
||||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
|
||||||
def get_config_context(self, obj):
|
|
||||||
return obj.get_config_context()
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceNAPALMSerializer(serializers.Serializer):
|
class DeviceNAPALMSerializer(serializers.Serializer):
|
||||||
method = serializers.JSONField()
|
method = serializers.JSONField()
|
||||||
|
|
||||||
@ -935,7 +936,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ['id', 'url', 'display', 'name', 'label']
|
fields = ['id', 'url', 'display', 'name', 'label', 'description']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
@ -1059,7 +1060,7 @@ class TracedCableSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -813,6 +813,8 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||||
|
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
||||||
|
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
||||||
|
|
||||||
# Ethernet Backplane
|
# Ethernet Backplane
|
||||||
TYPE_1GE_KX = '1000base-kx'
|
TYPE_1GE_KX = '1000base-kx'
|
||||||
@ -954,6 +956,8 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||||
|
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
||||||
|
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -3,7 +3,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import ASN, IPAddress, VRF
|
from ipam.models import ASN, L2VPN, IPAddress, VRF
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||||
)
|
)
|
||||||
@ -1414,6 +1414,17 @@ class InterfaceFilterSet(
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Virtual Device Context',
|
label='Virtual Device Context',
|
||||||
)
|
)
|
||||||
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
label=_('L2VPN (ID)'),
|
||||||
|
)
|
||||||
|
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn__identifier',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
to_field_name='identifier',
|
||||||
|
label=_('L2VPN'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -6,7 +6,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from ipam.models import ASN, VRF
|
from ipam.models import ASN, L2VPN, VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -1112,7 +1112,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
||||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
|
||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
|
||||||
@ -1203,6 +1203,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='VRF'
|
label='VRF'
|
||||||
)
|
)
|
||||||
|
l2vpn_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('L2VPN')
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class PowerFeedTable(CableTerminationTable):
|
|||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
|
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
|
||||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -21,7 +21,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
|
|||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||||
|
from virtualization.filtersets import VirtualMachineFilterSet
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
from virtualization.tables import VirtualMachineTable
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import DeviceFaceChoices
|
from .choices import DeviceFaceChoices
|
||||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||||
@ -1639,6 +1641,42 @@ class DeviceRoleView(generic.ObjectView):
|
|||||||
queryset = DeviceRole.objects.all()
|
queryset = DeviceRole.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(DeviceRole, 'devices', path='devices')
|
||||||
|
class DeviceRoleDevicesView(generic.ObjectChildrenView):
|
||||||
|
queryset = DeviceRole.objects.all()
|
||||||
|
child_model = Device
|
||||||
|
table = tables.DeviceTable
|
||||||
|
filterset = filtersets.DeviceFilterSet
|
||||||
|
template_name = 'dcim/devicerole/devices.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Devices'),
|
||||||
|
badge=lambda obj: obj.devices.count(),
|
||||||
|
permission='dcim.view_device',
|
||||||
|
weight=400
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return Device.objects.restrict(request.user, 'view').filter(device_role=parent)
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(DeviceRole, 'virtual_machines', path='virtual-machines')
|
||||||
|
class DeviceRoleVirtualMachinesView(generic.ObjectChildrenView):
|
||||||
|
queryset = DeviceRole.objects.all()
|
||||||
|
child_model = VirtualMachine
|
||||||
|
table = VirtualMachineTable
|
||||||
|
filterset = VirtualMachineFilterSet
|
||||||
|
template_name = 'dcim/devicerole/virtual_machines.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Virtual machines'),
|
||||||
|
badge=lambda obj: obj.virtual_machines.count(),
|
||||||
|
permission='virtualization.view_virtualmachine',
|
||||||
|
weight=500
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return VirtualMachine.objects.restrict(request.user, 'view').filter(role=parent)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceRole, 'edit')
|
@register_model_view(DeviceRole, 'edit')
|
||||||
class DeviceRoleEditView(generic.ObjectEditView):
|
class DeviceRoleEditView(generic.ObjectEditView):
|
||||||
queryset = DeviceRole.objects.all()
|
queryset = DeviceRole.objects.all()
|
||||||
|
@ -318,6 +318,10 @@ class ScriptViewSet(ViewSet):
|
|||||||
"""
|
"""
|
||||||
Run a Script identified as "<module>.<script>" and return the pending JobResult as the result
|
Run a Script identified as "<module>.<script>" and return the pending JobResult as the result
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not request.user.has_perm('extras.run_script'):
|
||||||
|
raise PermissionDenied("This user does not have permission to run scripts.")
|
||||||
|
|
||||||
script = self._get_script(pk)()
|
script = self._get_script(pk)()
|
||||||
input_serializer = serializers.ScriptInputSerializer(data=request.data)
|
input_serializer = serializers.ScriptInputSerializer(data=request.data)
|
||||||
|
|
||||||
|
@ -590,6 +590,7 @@ class ScriptTest(APITestCase):
|
|||||||
|
|
||||||
@skipIf(not rq_worker_running, "RQ worker not running")
|
@skipIf(not rq_worker_running, "RQ worker not running")
|
||||||
def test_run_script(self):
|
def test_run_script(self):
|
||||||
|
self.add_permissions('extras.run_script')
|
||||||
|
|
||||||
script_data = {
|
script_data = {
|
||||||
'var1': 'FooBar',
|
'var1': 'FooBar',
|
||||||
|
@ -852,6 +852,17 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
method='get_for_virtualmachine'
|
method='get_for_virtualmachine'
|
||||||
)
|
)
|
||||||
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
label=_('L2VPN (ID)'),
|
||||||
|
)
|
||||||
|
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn__identifier',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
to_field_name='identifier',
|
||||||
|
label=_('L2VPN'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
|
@ -413,7 +413,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attributes', ('group_id', 'status', 'role_id', 'vid')),
|
('Attributes', ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -458,6 +458,11 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='VLAN ID'
|
label='VLAN ID'
|
||||||
)
|
)
|
||||||
|
l2vpn_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('L2VPN')
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -160,7 +161,13 @@ class SearchView(View):
|
|||||||
lookup=lookup
|
lookup=lookup
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.cleaned_data['lookup'] != LookupTypes.EXACT:
|
# If performing a regex search, pass the highlight value as a compiled pattern
|
||||||
|
if form.cleaned_data['lookup'] == LookupTypes.REGEX:
|
||||||
|
try:
|
||||||
|
highlight = re.compile(f"({form.cleaned_data['q']})", flags=re.IGNORECASE)
|
||||||
|
except re.error:
|
||||||
|
pass
|
||||||
|
elif form.cleaned_data['lookup'] != LookupTypes.EXACT:
|
||||||
highlight = form.cleaned_data['q']
|
highlight = form.cleaned_data['q']
|
||||||
|
|
||||||
table = SearchTable(results, highlight=highlight)
|
table = SearchTable(results, highlight=highlight)
|
||||||
|
@ -71,13 +71,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">Devices</h5>
|
|
||||||
<div class="card-body htmx-container table-responsive"
|
|
||||||
hx-get="{% url 'dcim:device_list' %}?role_id={{ object.pk }}"
|
|
||||||
hx-trigger="load"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
21
netbox/templates/dcim/devicerole/devices.html
Normal file
21
netbox/templates/dcim/devicerole/devices.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'dcim/devicerole.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal='DeviceTable_config' %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
hx-get="{% url 'dcim:device_list' %}?role_id={{ object.pk }}"
|
||||||
|
hx-trigger="load"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% table_config_form table %}
|
||||||
|
{% endblock modals %}
|
21
netbox/templates/dcim/devicerole/virtual_machines.html
Normal file
21
netbox/templates/dcim/devicerole/virtual_machines.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'dcim/devicerole.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal='VirtualMachineTable_config' %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
hx-get="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}"
|
||||||
|
hx-trigger="load"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% table_config_form table %}
|
||||||
|
{% endblock modals %}
|
@ -62,7 +62,7 @@ class ContactTable(NetBoxTable):
|
|||||||
verbose_name='Assignments'
|
verbose_name='Assignments'
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='tenancy:tenant_list'
|
url_name='tenancy:contact_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
|
@ -40,7 +40,7 @@ class TenantTable(ContactsColumnMixin, NetBoxTable):
|
|||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='tenancy:contact_list'
|
url_name='tenancy:tenant_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
|
@ -27,7 +27,7 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
|||||||
def get_request_serializer(self):
|
def get_request_serializer(self):
|
||||||
serializer = super().get_request_serializer()
|
serializer = super().get_request_serializer()
|
||||||
|
|
||||||
if serializer is not None and self.method in self.implicit_body_methods:
|
if serializer is not None and not isinstance(serializer, openapi.Schema) and self.method in self.implicit_body_methods:
|
||||||
if writable_class := self.get_writable_class(serializer):
|
if writable_class := self.get_writable_class(serializer):
|
||||||
if hasattr(serializer, 'child'):
|
if hasattr(serializer, 'child'):
|
||||||
child_serializer = self.get_writable_class(serializer.child)
|
child_serializer = self.get_writable_class(serializer.child)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.constants import EMPTY_VALUES
|
from django_filters.constants import EMPTY_VALUES
|
||||||
|
|
||||||
|
|
||||||
@ -67,6 +68,12 @@ class MACAddressFilter(django_filters.CharFilter):
|
|||||||
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.CharField)
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
try:
|
||||||
|
return super().filter(qs, value)
|
||||||
|
except ValidationError:
|
||||||
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.CharField)
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
@ -514,11 +514,21 @@ def clean_html(html, schemes):
|
|||||||
def highlight_string(value, highlight, trim_pre=None, trim_post=None, trim_placeholder='...'):
|
def highlight_string(value, highlight, trim_pre=None, trim_post=None, trim_placeholder='...'):
|
||||||
"""
|
"""
|
||||||
Highlight a string within a string and optionally trim the pre/post portions of the original string.
|
Highlight a string within a string and optionally trim the pre/post portions of the original string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The body of text being searched against
|
||||||
|
highlight: The string of compiled regex pattern to highlight in `value`
|
||||||
|
trim_pre: Maximum length of pre-highlight text to include
|
||||||
|
trim_post: Maximum length of post-highlight text to include
|
||||||
|
trim_placeholder: String value to swap in for trimmed pre/post text
|
||||||
"""
|
"""
|
||||||
# Split value on highlight string
|
# Split value on highlight string
|
||||||
try:
|
try:
|
||||||
pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
|
if type(highlight) is re.Pattern:
|
||||||
except ValueError:
|
pre, match, post = highlight.split(value, maxsplit=1)
|
||||||
|
else:
|
||||||
|
pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
|
||||||
|
except ValueError as e:
|
||||||
# Match not found
|
# Match not found
|
||||||
return escape(value)
|
return escape(value)
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
class Meta(VirtualMachineSerializer.Meta):
|
class Meta(VirtualMachineSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
|
||||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import VRF
|
from ipam.models import L2VPN, VRF
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||||
@ -295,6 +295,17 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet):
|
|||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label=_('VRF (RD)'),
|
label=_('VRF (RD)'),
|
||||||
)
|
)
|
||||||
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
label=_('L2VPN (ID)'),
|
||||||
|
)
|
||||||
|
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn__identifier',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
to_field_name='identifier',
|
||||||
|
label=_('L2VPN'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from ipam.models import VRF
|
from ipam.models import L2VPN, VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -177,7 +177,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
|
('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
|
||||||
('Attributes', ('enabled', 'mac_address', 'vrf_id')),
|
('Attributes', ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
|
||||||
)
|
)
|
||||||
cluster_id = DynamicModelMultipleChoiceField(
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
@ -207,4 +207,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='VRF'
|
label='VRF'
|
||||||
)
|
)
|
||||||
|
l2vpn_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('L2VPN')
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
@ -10,7 +10,7 @@ django-prometheus==2.2.0
|
|||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
django-rich==1.4.0
|
django-rich==1.4.0
|
||||||
django-rq==2.6.0
|
django-rq==2.6.0
|
||||||
django-tables2==2.5.0
|
django-tables2==2.5.1
|
||||||
django-taggit==3.1.0
|
django-taggit==3.1.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
@ -19,13 +19,13 @@ graphene-django==3.0.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==8.5.11
|
mkdocs-material==9.0.6
|
||||||
mkdocstrings[python-legacy]==0.19.1
|
mkdocstrings[python-legacy]==0.20.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.4.0
|
Pillow==9.4.0
|
||||||
psycopg2-binary==2.9.5
|
psycopg2-binary==2.9.5
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.12.1
|
sentry-sdk==1.13.0
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core[openidconnect]==4.3.0
|
social-auth-core[openidconnect]==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user