Merge v2.11.2

This commit is contained in:
jeremystretch 2021-04-27 14:05:38 -04:00
commit 456ffb79ff
26 changed files with 138 additions and 73 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@ -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. 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 ```no-highlight
sudo echo napalm >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
``` ```
### Remote File Storage ### 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`. 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 ```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 ## Run the Upgrade Script

View File

@ -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: Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
```no-highlight ```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 ## Configuration

View File

@ -1,5 +1,27 @@
# NetBox v2.11 # 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) ## v2.11.1 (2021-04-21)
### Enhancements ### Enhancements
@ -175,6 +197,7 @@ A new provider network model has been introduced to represent the boundary of a
* circuits.CircuitTermination * circuits.CircuitTermination
* Added the `provider_network` field * Added the `provider_network` field
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields * Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
* The `trace/` endpoint has been replaced with `paths/`
* circuits.ProviderNetwork * circuits.ProviderNetwork
* Added the `/api/circuits/provider-networks/` endpoint * Added the `/api/circuits/provider-networks/` endpoint
* dcim.Device * dcim.Device

View File

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

View File

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

View File

@ -90,7 +90,7 @@ class RegionSerializer(NestedGroupModelSerializer):
class SiteGroupSerializer(NestedGroupModelSerializer): class SiteGroupSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') 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) site_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:

View File

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

View File

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

View File

@ -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),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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): class WebhookFilterSet(BaseFilterSet):
content_types = ContentTypeFilter() content_types = ContentTypeFilter()
http_method = django_filters.MultipleChoiceFilter( http_method = django_filters.MultipleChoiceFilter(
@ -119,7 +140,7 @@ class ImageAttachmentFilterSet(BaseFilterSet):
fields = ['id', 'content_type_id', 'object_id', 'name'] fields = ['id', 'content_type_id', 'object_id', 'name']
class JournalEntryFilterSet(BaseFilterSet): class JournalEntryFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -150,7 +171,7 @@ class JournalEntryFilterSet(BaseFilterSet):
return queryset.filter(comments__icontains=value) return queryset.filter(comments__icontains=value)
class TagFilterSet(BaseFilterSet): class TagFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -169,7 +190,7 @@ class TagFilterSet(BaseFilterSet):
) )
class ConfigContextFilterSet(BaseFilterSet): class ConfigContextFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='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 # Job Results
# #

View File

@ -431,7 +431,9 @@ class JournalEntry(ChangeLoggedModel):
verbose_name_plural = 'journal entries' verbose_name_plural = 'journal entries'
def __str__(self): 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): def get_absolute_url(self):
return reverse('extras:journalentry', args=[self.pk]) return reverse('extras:journalentry', args=[self.pk])

View File

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

View File

@ -1561,7 +1561,7 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
# Limit IP address choices to those assigned to interfaces of the parent device/VM # Limit IP address choices to those assigned to interfaces of the parent device/VM
if self.instance.device: if self.instance.device:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter( 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: elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter( self.fields['ipaddresses'].queryset = IPAddress.objects.filter(

View File

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

View File

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

View File

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

View File

@ -7,6 +7,11 @@
Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %} Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
</button> </button>
<ul class="dropdown-menu" aria-labelledby="max_length"> <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 %} {% 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 %}"> <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 %} {{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}

View File

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

View File

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

View File

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