jeremystretch 4 лет назад
Родитель
Сommit
456ffb79ff

+ 0 - 1
.github/FUNDING.yml

@@ -1 +0,0 @@
-github: [jeremystretch]

+ 2 - 2
docs/installation/3-netbox.md

@@ -198,7 +198,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
 The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
 
 ```no-highlight
-sudo echo napalm >> /opt/netbox/local_requirements.txt
+sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
 ```
 
 ### Remote File Storage
@@ -206,7 +206,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt
 By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
 
 ```no-highlight
-sudo echo django-storages >> /opt/netbox/local_requirements.txt
+sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
 ```
 
 ## Run the Upgrade Script

+ 1 - 1
docs/installation/6-ldap.md

@@ -30,7 +30,7 @@ pip3 install django-auth-ldap
 Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
 
 ```no-highlight
-sudo echo django-auth-ldap >> /opt/netbox/local_requirements.txt
+sudo sh -c "echo 'django-auth-ldap' >> /opt/netbox/local_requirements.txt"
 ```
 
 ## Configuration

+ 23 - 0
docs/release-notes/version-2.11.md

@@ -1,5 +1,27 @@
 # NetBox v2.11
 
+## v2.11.2 (2021-04-27)
+
+### Enhancements
+
+* [#6275](https://github.com/netbox-community/netbox/issues/6275) - Linkify rack, device counts on locations list
+* [#6278](https://github.com/netbox-community/netbox/issues/6278) - Note device locations on cable traces
+* [#6287](https://github.com/netbox-community/netbox/issues/6287) - Add option to clear assigned max length filter on prefixes list
+
+### Bug Fixes
+
+* [#6236](https://github.com/netbox-community/netbox/issues/6236) - Journal entry title should account for configured timezone
+* [#6246](https://github.com/netbox-community/netbox/issues/6246) - Permit full-length descriptions when creating device components and VM interfaces
+* [#6248](https://github.com/netbox-community/netbox/issues/6248) - Fix table column reconfiguration under Chrome
+* [#6252](https://github.com/netbox-community/netbox/issues/6252) - Fix assignment of console port speed values above 19.2kbps
+* [#6254](https://github.com/netbox-community/netbox/issues/6254) - Disable ordering of space column in racks table
+* [#6258](https://github.com/netbox-community/netbox/issues/6258) - Fix parent assignment for SiteGroup API serializer
+* [#6262](https://github.com/netbox-community/netbox/issues/6262) - Support filtering by created/updated time for all relevant objects
+* [#6267](https://github.com/netbox-community/netbox/issues/6267) - Fix cable tracing API endpoint for circuit terminations
+* [#6289](https://github.com/netbox-community/netbox/issues/6289) - Fix assignment of VC member interfaces to LAG interfaces
+
+---
+
 ## v2.11.1 (2021-04-21)
 
 ### Enhancements
@@ -175,6 +197,7 @@ A new provider network model has been introduced to represent the boundary of a
 * circuits.CircuitTermination
     * Added the `provider_network` field
     * Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
+    * The `trace/` endpoint has been replaced with `paths/`
 * circuits.ProviderNetwork
     * Added the `/api/circuits/provider-networks/` endpoint
 * dcim.Device

+ 2 - 2
netbox/circuits/api/views.py

@@ -2,7 +2,7 @@ from rest_framework.routers import APIRootView
 
 from circuits import filters
 from circuits.models import *
-from dcim.api.views import PathEndpointMixin
+from dcim.api.views import PassThroughPortMixin
 from extras.api.views import CustomFieldModelViewSet
 from netbox.api.views import ModelViewSet
 from utilities.utils import count_related
@@ -57,7 +57,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
 # Circuit Terminations
 #
 
-class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
+class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
     queryset = CircuitTermination.objects.prefetch_related(
         'circuit', 'site', 'provider_network', 'cable'
     )

+ 3 - 3
netbox/circuits/filters.py

@@ -1,7 +1,7 @@
 import django_filters
 from django.db.models import Q
 
-from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
+from dcim.filters import CableTerminationFilterSet
 from dcim.models import Region, Site, SiteGroup
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
@@ -110,7 +110,7 @@ class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, Created
         ).distinct()
 
 
-class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = CircuitType
@@ -207,7 +207,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
         ).distinct()
 
 
-class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
+class CircuitTerminationFilterSet(BaseFilterSet, CreatedUpdatedFilterSet, CableTerminationFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 1 - 1
netbox/dcim/api/serializers.py

@@ -90,7 +90,7 @@ class RegionSerializer(NestedGroupModelSerializer):
 
 class SiteGroupSerializer(NestedGroupModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
-    parent = NestedRegionSerializer(required=False, allow_null=True)
+    parent = NestedSiteGroupSerializer(required=False, allow_null=True)
     site_count = serializers.IntegerField(read_only=True)
 
     class Meta:

+ 15 - 15
netbox/dcim/filters.py

@@ -57,7 +57,7 @@ __all__ = (
 )
 
 
-class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         label='Parent region (ID)',
@@ -74,7 +74,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
 
 
-class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         label='Parent site group (ID)',
@@ -154,7 +154,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.filter(qs_filter)
 
 
-class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
@@ -218,7 +218,7 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         )
 
 
-class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = RackRole
@@ -323,7 +323,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         )
 
 
-class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
+class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -383,7 +383,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
         )
 
 
-class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = Manufacturer
@@ -476,7 +476,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
         return queryset.exclude(devicebaytemplates__isnull=value)
 
 
-class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
+class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceType.objects.all(),
         field_name='device_type_id',
@@ -556,14 +556,14 @@ class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name']
 
 
-class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = DeviceRole
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
-class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         field_name='manufacturer',
         queryset=Manufacturer.objects.all(),
@@ -792,7 +792,7 @@ class DeviceFilterSet(
         return queryset.exclude(devicebays__isnull=value)
 
 
-class DeviceComponentFilterSet(CustomFieldModelFilterSet):
+class DeviceComponentFilterSet(CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -984,7 +984,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
             devices = Device.objects.filter(**{'{}__in'.format(name): value})
             vc_interface_ids = []
             for device in devices:
-                vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
+                vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
             return queryset.filter(pk__in=vc_interface_ids)
         except Device.DoesNotExist:
             return queryset.none()
@@ -995,7 +995,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         try:
             devices = Device.objects.filter(pk__in=id_list)
             for device in devices:
-                vc_interface_ids += device.vc_interfaces.values_list('id', flat=True)
+                vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
             return queryset.filter(pk__in=vc_interface_ids)
         except Device.DoesNotExist:
             return queryset.none()
@@ -1129,7 +1129,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
         return queryset.filter(qs_filter)
 
 
-class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
+class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1209,7 +1209,7 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
         return queryset.filter(qs_filter).distinct()
 
 
-class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
+class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1340,7 +1340,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = []
 
 
-class PowerPanelFilterSet(BaseFilterSet):
+class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 2 - 2
netbox/dcim/forms.py

@@ -2153,7 +2153,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
                 ip_choices = [(None, '---------')]
 
                 # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
-                interface_ids = self.instance.vc_interfaces.values_list('pk', flat=True)
+                interface_ids = self.instance.vc_interfaces().values_list('pk', flat=True)
 
                 # Collect interface IPs
                 interface_ips = IPAddress.objects.filter(
@@ -2552,7 +2552,7 @@ class ComponentCreateForm(BootstrapMixin, CustomFieldForm, ComponentForm):
         queryset=Device.objects.all()
     )
     description = forms.CharField(
-        max_length=100,
+        max_length=200,
         required=False
     )
     tags = DynamicModelMultipleChoiceField(

+ 21 - 0
netbox/dcim/migrations/0131_consoleport_speed.py

@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0130_sitegroup'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='consoleport',
+            name='speed',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverport',
+            name='speed',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+    ]

+ 2 - 2
netbox/dcim/models/device_components.py

@@ -222,7 +222,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
         blank=True,
         help_text='Physical port type'
     )
-    speed = models.PositiveSmallIntegerField(
+    speed = models.PositiveIntegerField(
         choices=ConsolePortSpeedChoices,
         blank=True,
         null=True,
@@ -265,7 +265,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
         blank=True,
         help_text='Physical port type'
     )
-    speed = models.PositiveSmallIntegerField(
+    speed = models.PositiveIntegerField(
         choices=ConsolePortSpeedChoices,
         blank=True,
         null=True,

+ 11 - 4
netbox/dcim/models/devices.py

@@ -716,7 +716,7 @@ class Device(PrimaryModel, ConfigContextModel):
                 pass
 
         # Validate primary IP addresses
-        vc_interfaces = self.vc_interfaces.all()
+        vc_interfaces = self.vc_interfaces()
         if self.primary_ip4:
             if self.primary_ip4.family != 4:
                 raise ValidationError({
@@ -854,20 +854,27 @@ class Device(PrimaryModel, ConfigContextModel):
         else:
             return None
 
+    @property
+    def interfaces_count(self):
+        if self.virtual_chassis and self.virtual_chassis.master == self:
+            return self.vc_interfaces().count()
+        return self.interfaces.count()
+
     def get_vc_master(self):
         """
         If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
         """
         return self.virtual_chassis.master if self.virtual_chassis else None
 
-    @property
-    def vc_interfaces(self):
+    def vc_interfaces(self, if_master=False):
         """
         Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
         Device belonging to the same VirtualChassis.
+
+        :param if_master: If True, return VC member interfaces only if this Device is the VC master.
         """
         filter = Q(device=self)
-        if self.virtual_chassis and self.virtual_chassis.master == self:
+        if self.virtual_chassis and (not if_master or self.virtual_chassis.master == self):
             filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
         return Interface.objects.filter(filter)
 

+ 1 - 0
netbox/dcim/tables/racks.py

@@ -73,6 +73,7 @@ class RackDetailTable(RackTable):
         verbose_name='Devices'
     )
     get_utilization = UtilizationColumn(
+        orderable=False,
         verbose_name='Space'
     )
     get_power_utilization = UtilizationColumn(

+ 6 - 2
netbox/dcim/tables/sites.py

@@ -102,10 +102,14 @@ class LocationTable(BaseTable):
     site = tables.Column(
         linkify=True
     )
-    rack_count = tables.Column(
+    rack_count = LinkedCountColumn(
+        viewname='dcim:rack_list',
+        url_params={'location_id': 'pk'},
         verbose_name='Racks'
     )
-    device_count = tables.Column(
+    device_count = LinkedCountColumn(
+        viewname='dcim:device_list',
+        url_params={'location_id': 'pk'},
         verbose_name='Devices'
     )
     actions = ButtonsColumn(

+ 2 - 2
netbox/dcim/views.py

@@ -1405,7 +1405,7 @@ class DeviceInterfacesView(generic.ObjectView):
     template_name = 'dcim/device/interfaces.html'
 
     def get_extra_context(self, request, instance):
-        interfaces = instance.vc_interfaces.restrict(request.user, 'view').prefetch_related(
+        interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
             'lag', 'cable', '_path__destination', 'tags',
@@ -1527,7 +1527,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
     template_name = 'dcim/device/lldp_neighbors.html'
 
     def get_extra_context(self, request, instance):
-        interfaces = instance.vc_interfaces.restrict(request.user, 'view').prefetch_related(
+        interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
             '_path__destination'
         ).exclude(
             type__in=NONCONNECTABLE_IFACE_TYPES

+ 24 - 24
netbox/extras/filters.py

@@ -36,6 +36,27 @@ EXACT_FILTER_TYPES = (
 )
 
 
+class CreatedUpdatedFilterSet(django_filters.FilterSet):
+    created = django_filters.DateFilter()
+    created__gte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='gte'
+    )
+    created__lte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='lte'
+    )
+    last_updated = django_filters.DateTimeFilter()
+    last_updated__gte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='gte'
+    )
+    last_updated__lte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='lte'
+    )
+
+
 class WebhookFilterSet(BaseFilterSet):
     content_types = ContentTypeFilter()
     http_method = django_filters.MultipleChoiceFilter(
@@ -119,7 +140,7 @@ class ImageAttachmentFilterSet(BaseFilterSet):
         fields = ['id', 'content_type_id', 'object_id', 'name']
 
 
-class JournalEntryFilterSet(BaseFilterSet):
+class JournalEntryFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -150,7 +171,7 @@ class JournalEntryFilterSet(BaseFilterSet):
         return queryset.filter(comments__icontains=value)
 
 
-class TagFilterSet(BaseFilterSet):
+class TagFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -169,7 +190,7 @@ class TagFilterSet(BaseFilterSet):
         )
 
 
-class ConfigContextFilterSet(BaseFilterSet):
+class ConfigContextFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -341,27 +362,6 @@ class ObjectChangeFilterSet(BaseFilterSet):
         )
 
 
-class CreatedUpdatedFilterSet(django_filters.FilterSet):
-    created = django_filters.DateFilter()
-    created__gte = django_filters.DateFilter(
-        field_name='created',
-        lookup_expr='gte'
-    )
-    created__lte = django_filters.DateFilter(
-        field_name='created',
-        lookup_expr='lte'
-    )
-    last_updated = django_filters.DateTimeFilter()
-    last_updated__gte = django_filters.DateTimeFilter(
-        field_name='last_updated',
-        lookup_expr='gte'
-    )
-    last_updated__lte = django_filters.DateTimeFilter(
-        field_name='last_updated',
-        lookup_expr='lte'
-    )
-
-
 #
 # Job Results
 #

+ 3 - 1
netbox/extras/models/models.py

@@ -431,7 +431,9 @@ class JournalEntry(ChangeLoggedModel):
         verbose_name_plural = 'journal entries'
 
     def __str__(self):
-        return f"{date_format(self.created)} - {time_format(self.created)} ({self.get_kind_display()})"
+        created_date = timezone.localdate(self.created)
+        created_time = timezone.localtime(self.created)
+        return f"{date_format(created_date)} - {time_format(created_time)} ({self.get_kind_display()})"
 
     def get_absolute_url(self):
         return reverse('extras:journalentry', args=[self.pk])

+ 4 - 4
netbox/ipam/filters.py

@@ -116,7 +116,7 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
         fields = ['id', 'name']
 
 
-class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = RIR
@@ -173,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
             return queryset.none()
 
 
-class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -515,7 +515,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
             return queryset.none()
         interface_ids = []
         for device in devices:
-            interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
+            interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
         return queryset.filter(
             interface__in=interface_ids
         )
@@ -535,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
         return queryset.exclude(assigned_object_id__isnull=value)
 
 
-class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     scope_type = ContentTypeFilter()
     region = django_filters.NumberFilter(
         method='filter_scope'

+ 1 - 1
netbox/ipam/forms.py

@@ -1561,7 +1561,7 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
         # Limit IP address choices to those assigned to interfaces of the parent device/VM
         if self.instance.device:
             self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
-                interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
+                interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True)
             )
         elif self.instance.virtual_machine:
             self.fields['ipaddresses'].queryset = IPAddress.objects.filter(

+ 1 - 1
netbox/secrets/filters.py

@@ -14,7 +14,7 @@ __all__ = (
 )
 
 
-class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = SecretRole

+ 1 - 1
netbox/templates/dcim/device/base.html

@@ -119,7 +119,7 @@
         Device
     </a>
 </li>
-{% with interface_count=object.vc_interfaces.count %}
+{% with interface_count=object.interfaces_count %}
 {% if interface_count %}
 <li role="presentation" class="nav-item">
     <a class="nav-link {% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>

+ 3 - 0
netbox/templates/dcim/trace/device.html

@@ -2,6 +2,9 @@
     <strong><a href="{{ device.get_absolute_url }}">{{ device }}</a></strong><br />
     {{ device.device_type.manufacturer }} {{ device.device_type }}<br />
     <a href="{{ device.site.get_absolute_url }}">{{ device.site }}</a>
+    {% if device.location %}
+        / <a href="{{ device.location.get_absolute_url }}">{{ device.location }}</a>
+    {% endif %}
     {% if device.rack %}
         / <a href="{{ device.rack.get_absolute_url }}">{{ device.rack }}</a>
     {% endif %}

+ 5 - 0
netbox/templates/ipam/prefix_list.html

@@ -7,6 +7,11 @@
             Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
         </button>
         <ul class="dropdown-menu" aria-labelledby="max_length">
+            {% if request.GET.mask_length__lte %}
+                <li>
+                    <a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=None page=1 %}">Clear</a>
+                </li>
+            {% endif %}
             {% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
                 <li><a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
                     {{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}

+ 1 - 1
netbox/tenancy/filters.py

@@ -13,7 +13,7 @@ __all__ = (
 )
 
 
-class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         label='Tenant group (ID)',

+ 2 - 2
netbox/virtualization/filters.py

@@ -20,14 +20,14 @@ __all__ = (
 )
 
 
-class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = ClusterType
         fields = ['id', 'name', 'slug', 'description']
 
 
-class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
 
     class Meta:
         model = ClusterGroup

+ 1 - 1
netbox/virtualization/forms.py

@@ -682,7 +682,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
         label='MAC Address'
     )
     description = forms.CharField(
-        max_length=100,
+        max_length=200,
         required=False
     )
     mode = forms.ChoiceField(