Merge branch 'netbox-community:develop' into develop
This commit is contained in:
commit
2d7e33df3b
|
@ -67,7 +67,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
|||
|
||||
Default: `|` (Pipe)
|
||||
|
||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
The Separator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -390,7 +390,7 @@ class NewBranchScript(Script):
|
|||
name=f'{site.slug}-switch{i}',
|
||||
site=site,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
role=switch_role
|
||||
device_role=switch_role
|
||||
)
|
||||
switch.full_clean()
|
||||
switch.save()
|
||||
|
|
|
@ -2,6 +2,17 @@
|
|||
|
||||
## v3.7.3 (FUTURE)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#15059](https://github.com/netbox-community/netbox/issues/15059) - Correct IP address count link in VM interfaces table
|
||||
* [#15067](https://github.com/netbox-community/netbox/issues/15067) - Fix uncaught exception when attempting invalid device bay import
|
||||
* [#15070](https://github.com/netbox-community/netbox/issues/15070) - Fix inclusion of `config_template` field on REST API serializer for virtual machines
|
||||
* [#15084](https://github.com/netbox-community/netbox/issues/15084) - Fix "add export template" link under "export" button on object list views
|
||||
* [#15091](https://github.com/netbox-community/netbox/issues/15091) - Fix designation of the active tab for assigned object when modifying an L2VPN termination
|
||||
* [#15115](https://github.com/netbox-community/netbox/issues/15115) - Fix unhandled exception with invalid permission constraints
|
||||
* [#15126](https://github.com/netbox-community/netbox/issues/15126) - `group` field should be optional when creating VPN tunnel via REST API
|
||||
* [#15133](https://github.com/netbox-community/netbox/issues/15133) - Fix FHRP group representation on assignments REST API endpoint using brief mode
|
||||
|
||||
---
|
||||
|
||||
## v3.7.2 (2024-02-05)
|
||||
|
|
|
@ -8,6 +8,7 @@ from drf_spectacular.plumbing import (
|
|||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
|
|
|
@ -996,7 +996,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
|
|||
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||
).exclude(pk=device.pk)
|
||||
else:
|
||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
||||
self.fields['installed_device'].queryset = Device.objects.none()
|
||||
|
||||
|
||||
class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
|
|
|
@ -1133,13 +1133,13 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
|
|||
super().clean()
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
if not self.device.device_type.is_parent_device:
|
||||
if hasattr(self, 'device') and not self.device.device_type.is_parent_device:
|
||||
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
|
||||
device_type=self.device.device_type
|
||||
))
|
||||
|
||||
# Cannot install a device into itself, obviously
|
||||
if self.device == self.installed_device:
|
||||
if self.installed_device and getattr(self, 'device', None) == self.installed_device:
|
||||
raise ValidationError(_("Cannot install a device into itself."))
|
||||
|
||||
# Check that the installed device is not already installed elsewhere
|
||||
|
|
|
@ -875,7 +875,7 @@ class Device(
|
|||
if self.position and self.device_type.u_height == 0:
|
||||
raise ValidationError({
|
||||
'position': _(
|
||||
"A U0 device type ({device_type}) cannot be assigned to a rack position."
|
||||
"A 0U device type ({device_type}) cannot be assigned to a rack position."
|
||||
).format(device_type=self.device_type)
|
||||
})
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ DEVICEBAY_STATUS = """
|
|||
INTERFACE_IPADDRESSES = """
|
||||
<div class="table-badge-group">
|
||||
{% if value.count >= 3 %}
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?interface_id={{ record.pk }}">{{ value.count }}</a>
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?{{ record|meta:"model_name" }}_id={{ record.pk }}">{{ value.count }}</a>
|
||||
{% else %}
|
||||
{% for ip in value.all %}
|
||||
{% if ip.status != 'active' %}
|
||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
|||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -15,6 +16,7 @@ from extras.models import EventRule
|
|||
from extras.validators import CustomValidator
|
||||
from netbox.config import get_config
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from netbox.signals import post_clean
|
||||
from utilities.exceptions import AbortRequest
|
||||
from .choices import ObjectChangeActionChoices
|
||||
|
@ -68,7 +70,7 @@ def handle_changed_object(sender, instance, **kwargs):
|
|||
else:
|
||||
return
|
||||
|
||||
# Create/update an ObejctChange record for this change
|
||||
# Create/update an ObjectChange record for this change
|
||||
objectchange = instance.to_objectchange(action)
|
||||
# If this is a many-to-many field change, check for a previous ObjectChange instance recorded
|
||||
# for this object by this request and update it
|
||||
|
@ -122,6 +124,25 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
# Django does not automatically send an m2m_changed signal for the reverse direction of a
|
||||
# many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
|
||||
# trigger one manually. We do this by checking for any reverse M2M relationships on the
|
||||
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
|
||||
# the association. This triggers an m2m_changed signal with the `post_remove` action type
|
||||
# for the forward direction of the relationship, ensuring that the change is recorded.
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) is not ManyToManyRel:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
|
||||
# Enqueue webhooks
|
||||
queue = events_queue.get()
|
||||
enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
|
|
|
@ -116,10 +116,11 @@ class NestedFHRPGroupSerializer(WritableNestedSerializer):
|
|||
|
||||
class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
|
||||
group = NestedFHRPGroupSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.FHRPGroupAssignment
|
||||
fields = ['id', 'url', 'display', 'interface_type', 'interface_id', 'group_id', 'priority']
|
||||
fields = ['id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority']
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -760,7 +760,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
|
|||
|
||||
class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
model = FHRPGroupAssignment
|
||||
brief_fields = ['display', 'group_id', 'id', 'interface_id', 'interface_type', 'priority', 'url']
|
||||
brief_fields = ['display', 'group', 'id', 'interface_id', 'interface_type', 'priority', 'url']
|
||||
bulk_update_data = {
|
||||
'priority': 100,
|
||||
}
|
||||
|
|
|
@ -489,10 +489,10 @@ class SyncedDataMixin(models.Model):
|
|||
# Create/delete AutoSyncRecord as needed
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
if self.auto_sync_enabled:
|
||||
AutoSyncRecord.objects.get_or_create(
|
||||
datafile=self.data_file,
|
||||
AutoSyncRecord.objects.update_or_create(
|
||||
object_type=content_type,
|
||||
object_id=self.pk
|
||||
object_id=self.pk,
|
||||
defaults={'datafile': self.data_file}
|
||||
)
|
||||
else:
|
||||
AutoSyncRecord.objects.filter(
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="offset-sm-3">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<li role="presentation" class="nav-item">
|
||||
<button role="tab" type="button" id="vlan_tab" data-bs-toggle="tab" aria-controls="vlan" data-bs-target="#vlan" class="nav-link {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}">
|
||||
<button role="tab" type="button" id="vlan_tab" data-bs-toggle="tab" aria-controls="vlan" data-bs-target="#vlan" class="nav-link {% if not form.initial.interface and not form.initial.vminterface %}active{% endif %}">
|
||||
{% trans "VLAN" %}
|
||||
</button>
|
||||
</li>
|
||||
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="tab-content p-0 border-0">
|
||||
<div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
|
||||
<div class="tab-pane {% if not form.initial.interface and not form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
|
||||
{% render_field form.vlan %}
|
||||
</div>
|
||||
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
|
||||
|
|
|
@ -385,7 +385,7 @@ class ObjectPermissionForm(BootstrapMixin, forms.ModelForm):
|
|||
CONSTRAINT_TOKEN_USER: 0, # Replace token with a null user ID
|
||||
}
|
||||
model.objects.filter(qs_filter_from_constraints(constraints, tokens)).exists()
|
||||
except FieldError as e:
|
||||
except (FieldError, ValueError) as e:
|
||||
raise forms.ValidationError({
|
||||
'constraints': _('Invalid filter for {model}: {error}').format(model=model, error=e)
|
||||
})
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'extras:exporttemplate_add' %}?content_type={{ content_type.pk }}">{% trans "Add export template" %}...</a>
|
||||
<a class="dropdown-item" href="{% url 'extras:exporttemplate_add' %}?content_types={{ content_type.pk }}">{% trans "Add export template" %}...</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
@ -103,8 +103,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||
fields = [
|
||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
|
||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'interface_count', 'virtual_disk_count',
|
||||
'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created',
|
||||
'last_updated', 'interface_count', 'virtual_disk_count',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
|
|
|
@ -46,7 +46,10 @@ class TunnelSerializer(NetBoxModelSerializer):
|
|||
status = ChoiceField(
|
||||
choices=TunnelStatusChoices
|
||||
)
|
||||
group = NestedTunnelGroupSerializer()
|
||||
group = NestedTunnelGroupSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
encapsulation = ChoiceField(
|
||||
choices=TunnelEncapsulationChoices
|
||||
)
|
||||
|
|
|
@ -40,6 +40,10 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
|
|||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
verbose_name=_('Group'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status')
|
||||
)
|
||||
|
@ -63,10 +67,10 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
|
|||
class Meta(NetBoxTable.Meta):
|
||||
model = Tunnel
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'tunnel_id',
|
||||
'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'group', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group',
|
||||
'tunnel_id', 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'encapsulation', 'tenant', 'terminations_count')
|
||||
default_columns = ('pk', 'name', 'group', 'status', 'encapsulation', 'tenant', 'terminations_count')
|
||||
|
||||
|
||||
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
|
|
|
@ -105,7 +105,6 @@ class TunnelTest(APIViewTestCases.APIViewTestCase):
|
|||
{
|
||||
'name': 'Tunnel 6',
|
||||
'status': TunnelStatusChoices.STATUS_DISABLED,
|
||||
'group': tunnel_groups[1].pk,
|
||||
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
|
||||
},
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue