diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 5b4e4e5a4..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: [jeremystretch] diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 649898e0a..7a8e0bc80 100644 --- a/docs/installation/3-netbox.md +++ b/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 diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 5d5135b33..27bdb0b40 100644 --- a/docs/installation/6-ldap.md +++ b/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 diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 838022006..a43e354de 100644 --- a/docs/release-notes/version-2.11.md +++ b/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 diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index c037bc5fd..0ea8d1973 100644 --- a/netbox/circuits/api/views.py +++ b/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' ) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f5d81c7bd..034a99ac9 100644 --- a/netbox/circuits/filters.py +++ b/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', diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index e9efe4136..377449140 100644 --- a/netbox/dcim/api/serializers.py +++ b/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: diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 2dc4faefb..29c4281ba 100644 --- a/netbox/dcim/filters.py +++ b/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', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9f4916d4b..16e909895 100644 --- a/netbox/dcim/forms.py +++ b/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( diff --git a/netbox/dcim/migrations/0131_consoleport_speed.py b/netbox/dcim/migrations/0131_consoleport_speed.py new file mode 100644 index 000000000..350162218 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index eda262a59..f2b13ed6f 100644 --- a/netbox/dcim/models/device_components.py +++ b/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, diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 551fac2d4..2fe7c28e5 100644 --- a/netbox/dcim/models/devices.py +++ b/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) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 3a63eef1e..ef0517dfc 100644 --- a/netbox/dcim/tables/racks.py +++ b/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( diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index b7d46eba5..47c912354 100644 --- a/netbox/dcim/tables/sites.py +++ b/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( diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a9aee80f1..69e043425 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 4b5c42eeb..aacdbda6b 100644 --- a/netbox/extras/filters.py +++ b/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 # diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index b3bc7f535..c2cebe163 100644 --- a/netbox/extras/models/models.py +++ b/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]) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 141d50139..8f4030411 100644 --- a/netbox/ipam/filters.py +++ b/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' diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 63e093422..6a3753859 100644 --- a/netbox/ipam/forms.py +++ b/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( diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 0cf4c2045..fb36c827a 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -14,7 +14,7 @@ __all__ = ( ) -class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): +class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): class Meta: model = SecretRole diff --git a/netbox/templates/dcim/device/base.html b/netbox/templates/dcim/device/base.html index 5355d5059..32a637c57 100644 --- a/netbox/templates/dcim/device/base.html +++ b/netbox/templates/dcim/device/base.html @@ -119,7 +119,7 @@ Device -{% with interface_count=object.vc_interfaces.count %} +{% with interface_count=object.interfaces_count %} {% if interface_count %}