mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-22 05:12:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
@@ -4,6 +4,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from ipam.models import ASN, L2VPN, IPAddress, VRF
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
@@ -818,7 +819,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
class DeviceFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
TenancyFilterSet,
|
||||
ContactModelFilterSet,
|
||||
LocalConfigContextFilterSet,
|
||||
PrimaryIPFilterSet,
|
||||
):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -994,16 +1001,6 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
method='_device_bays',
|
||||
label=_('Has device bays'),
|
||||
)
|
||||
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip4',
|
||||
queryset=IPAddress.objects.all(),
|
||||
label=_('Primary IPv4 (ID)'),
|
||||
)
|
||||
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip6',
|
||||
queryset=IPAddress.objects.all(),
|
||||
label=_('Primary IPv6 (ID)'),
|
||||
)
|
||||
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='oob_ip',
|
||||
queryset=IPAddress.objects.all(),
|
||||
@@ -1070,7 +1067,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
return queryset.exclude(devicebays__isnull=value)
|
||||
|
||||
|
||||
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
queryset=Device.objects.all(),
|
||||
|
||||
@@ -443,7 +443,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
platform = DynamicModelChoiceField(
|
||||
label=_('Platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
selector=True
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
label=_('Cluster'),
|
||||
|
||||
@@ -151,6 +151,23 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
)
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
rearport_count = len(self.cleaned_data['rear_port'])
|
||||
if frontport_count != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port': _(
|
||||
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||
"number of rear port positions ({rearport_count})."
|
||||
).format(
|
||||
frontport_count=frontport_count,
|
||||
rearport_count=rearport_count
|
||||
)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
@@ -291,6 +308,22 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
)
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
rearport_count = len(self.cleaned_data['rear_port'])
|
||||
if frontport_count != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port': _(
|
||||
"The number of front ports to be created ({frontport_count}) must match the selected number of "
|
||||
"rear port positions ({rearport_count})."
|
||||
).format(
|
||||
frontport_count=frontport_count,
|
||||
rearport_count=rearport_count
|
||||
)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
|
||||
@@ -180,6 +180,17 @@ class Cable(PrimaryModel):
|
||||
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
||||
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
||||
|
||||
if a_type == b_type:
|
||||
# can't directly use self.a_terminations here as possible they
|
||||
# don't have pk yet
|
||||
a_pks = set(obj.pk for obj in self.a_terminations if obj.pk)
|
||||
b_pks = set(obj.pk for obj in self.b_terminations if obj.pk)
|
||||
|
||||
if (a_pks & b_pks):
|
||||
raise ValidationError(
|
||||
_("A and B terminations cannot connect to the same object.")
|
||||
)
|
||||
|
||||
# Run clean() on any new CableTerminations
|
||||
for termination in self.a_terminations:
|
||||
CableTermination(cable=self, cable_end='A', termination=termination).clean()
|
||||
|
||||
@@ -466,6 +466,12 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
maximum_draw = tables.Column(
|
||||
verbose_name=_('Maximum draw (W)')
|
||||
)
|
||||
allocated_draw = tables.Column(
|
||||
verbose_name=_('Allocated draw (W)')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:powerport_list'
|
||||
)
|
||||
@@ -625,6 +631,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
verbose_name=_('VRF'),
|
||||
linkify=True
|
||||
)
|
||||
inventory_items = tables.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name=_('Inventory Items'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:interface_list'
|
||||
)
|
||||
@@ -636,7 +646,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
|
||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@@ -933,6 +943,10 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
discovered = columns.BooleanColumn(
|
||||
verbose_name=_('Discovered'),
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Parent'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:inventoryitem_list'
|
||||
)
|
||||
@@ -941,7 +955,7 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -87,6 +87,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Tenant')
|
||||
)
|
||||
site = tables.Column(
|
||||
accessor='rack__site',
|
||||
linkify=True,
|
||||
verbose_name=_('Site'),
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
@@ -97,9 +102,9 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',
|
||||
'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
|
||||
'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
|
||||
@@ -4712,12 +4712,18 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
addresses = (
|
||||
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
|
||||
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
|
||||
IPAddress(assigned_object=None, address='10.1.1.3/24'),
|
||||
IPAddress(assigned_object=interfaces[0], address='2001:db8::1/64'),
|
||||
IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
|
||||
IPAddress(assigned_object=None, address='2001:db8::3/64'),
|
||||
)
|
||||
IPAddress.objects.bulk_create(addresses)
|
||||
|
||||
vdcs[0].primary_ip4 = addresses[0]
|
||||
vdcs[0].primary_ip6 = addresses[3]
|
||||
vdcs[0].save()
|
||||
vdcs[1].primary_ip4 = addresses[1]
|
||||
vdcs[1].primary_ip6 = addresses[4]
|
||||
vdcs[1].save()
|
||||
|
||||
def test_device(self):
|
||||
@@ -4738,3 +4744,17 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'has_primary_ip': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_primary_ip4(self):
|
||||
addresses = IPAddress.objects.filter(address__family=4)
|
||||
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'primary_ip4_id': [addresses[2].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||
|
||||
def test_primary_ip6(self):
|
||||
addresses = IPAddress.objects.filter(address__family=6)
|
||||
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'primary_ip6_id': [addresses[2].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||
|
||||
@@ -2960,6 +2960,25 @@ class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||
|
||||
|
||||
@register_model_view(InventoryItem, 'children')
|
||||
class InventoryItemChildrenView(generic.ObjectChildrenView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
child_model = InventoryItem
|
||||
table = tables.InventoryItemTable
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('Children'),
|
||||
badge=lambda obj: obj.child_items.count(),
|
||||
permission='dcim.view_inventoryitem',
|
||||
hide_if_empty=True,
|
||||
weight=5000
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.child_items.restrict(request.user, 'view')
|
||||
|
||||
|
||||
#
|
||||
# Inventory item roles
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user