mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Merge v2.11.2
This commit is contained in:
commit
456ffb79ff
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
||||
github: [jeremystretch]
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,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'
|
||||
)
|
||||
|
@ -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',
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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
netbox/dcim/migrations/0131_consoleport_speed.py
Normal file
21
netbox/dcim/migrations/0131_consoleport_speed.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -73,6 +73,7 @@ class RackDetailTable(RackTable):
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = UtilizationColumn(
|
||||
orderable=False,
|
||||
verbose_name='Space'
|
||||
)
|
||||
get_power_utilization = UtilizationColumn(
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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])
|
||||
|
@ -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'
|
||||
|
@ -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(
|
||||
|
@ -14,7 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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)',
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user