diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index fdbeeade8..30a276c7d 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -622,8 +622,7 @@ class BaseInterface(models.Model):
@extras_features('graphs', 'export_templates', 'webhooks')
class Interface(CableTermination, ComponentModel, BaseInterface):
"""
- A network interface within a Device. A physical Interface can connect to exactly one other
- Interface.
+ A network interface within a Device. A physical Interface can connect to exactly one other Interface.
"""
device = models.ForeignKey(
to='Device',
@@ -695,8 +694,7 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
tags = TaggableManager(through=TaggedItem)
csv_headers = [
- 'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
- 'description', 'mode',
+ 'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
]
class Meta:
diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py
index 0a3c67f32..1ad355aec 100644
--- a/netbox/ipam/constants.py
+++ b/netbox/ipam/constants.py
@@ -33,7 +33,7 @@ PREFIX_LENGTH_MAX = 127 # IPv6
IPADDRESS_ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='interface') |
- Q(app_label='virtualization', model='interface')
+ Q(app_label='virtualization', model='vminterface')
)
IPADDRESS_MASK_LENGTH_MIN = 1
diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py
index c9012cd3a..0876a0d1a 100644
--- a/netbox/ipam/filters.py
+++ b/netbox/ipam/filters.py
@@ -319,6 +319,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
field_name='pk',
label='Virtual machine (ID)',
)
+ # TODO: Restore filtering by assigned interface
# interface = django_filters.ModelMultipleChoiceFilter(
# field_name='interface__name',
# queryset=Interface.objects.unrestricted(),
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index db9cc512e..ecb2d7c1e 100644
--- a/netbox/ipam/forms.py
+++ b/netbox/ipam/forms.py
@@ -522,6 +522,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
#
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
+ # TODO: Restore ability to select assigned object when editing IPAddress
# interface = forms.ModelChoiceField(
# queryset=Interface.objects.all(),
# required=False
diff --git a/netbox/ipam/migrations/0037_ipaddress_assignment.py b/netbox/ipam/migrations/0037_ipaddress_assignment.py
index 607f832a5..6139d41d6 100644
--- a/netbox/ipam/migrations/0037_ipaddress_assignment.py
+++ b/netbox/ipam/migrations/0037_ipaddress_assignment.py
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='ipaddress',
name='assigned_object_type',
- field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'interface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType', blank=True, null=True),
+ field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
preserve_default=False,
),
migrations.RunPython(
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index c7baba435..d2df6261f 100644
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -753,6 +753,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
super().save(*args, **kwargs)
def to_objectchange(self, action):
+ # Annotate the assigned object, if any
return ObjectChange(
changed_object=self,
object_repr=str(self),
@@ -764,12 +765,15 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
def to_csv(self):
# Determine if this IP is primary for a Device
+ is_primary = False
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
is_primary = True
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True
- else:
- is_primary = False
+
+ obj_type = None
+ if self.assigned_object_type:
+ obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}'
return (
self.address,
@@ -777,7 +781,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.get_role_display(),
- '{}.{}'.format(self.assigned_object_type.app_label, self.assigned_object_type.model) if self.assigned_object_type else None,
+ obj_type,
self.assigned_object_id,
is_primary,
self.dns_name,
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index 064b8d7ce..f1855327b 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -92,14 +92,6 @@ IPADDRESS_ASSIGN_LINK = """
{% endif %}
"""
-IPADDRESS_PARENT = """
-{% if record.interface %}
- {{ record.interface.parent }}
-{% else %}
- —
-{% endif %}
-"""
-
VRF_LINK = """
{% if record.vrf %}
{{ record.vrf }}
@@ -477,17 +469,13 @@ class IPAddressAssignTable(BaseTable):
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
- parent = tables.TemplateColumn(
- template_code=IPADDRESS_PARENT,
- orderable=False
- )
assigned_object = tables.Column(
orderable=False
)
class Meta(BaseTable.Meta):
model = IPAddress
- fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'assigned_object', 'description')
+ fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
orderable = False
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index bb8de1f73..bcbe00923 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -607,47 +607,47 @@ class IPAddressView(ObjectView):
ipaddress = get_object_or_404(self.queryset, pk=pk)
- # # Parent prefixes table
- # parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
- # vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
- # ).prefetch_related(
- # 'site', 'role'
- # )
- # parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
- # parent_prefixes_table.exclude = ('vrf',)
- #
- # # Duplicate IPs table
- # duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
- # vrf=ipaddress.vrf, address=str(ipaddress.address)
- # ).exclude(
- # pk=ipaddress.pk
- # ).prefetch_related(
- # 'nat_inside'
- # )
- # # Exclude anycast IPs if this IP is anycast
- # if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
- # duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
- # duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
- #
- # # Related IP table
- # related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
- # address=str(ipaddress.address)
- # ).filter(
- # vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
- # )
- # related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
- #
- # paginate = {
- # 'paginator_class': EnhancedPaginator,
- # 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
- # }
- # RequestConfig(request, paginate).configure(related_ips_table)
+ # Parent prefixes table
+ parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
+ vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
+ ).prefetch_related(
+ 'site', 'role'
+ )
+ parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
+ parent_prefixes_table.exclude = ('vrf',)
+
+ # Duplicate IPs table
+ duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
+ vrf=ipaddress.vrf, address=str(ipaddress.address)
+ ).exclude(
+ pk=ipaddress.pk
+ ).prefetch_related(
+ 'nat_inside'
+ )
+ # Exclude anycast IPs if this IP is anycast
+ if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
+ duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
+ duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
+
+ # Related IP table
+ related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
+ address=str(ipaddress.address)
+ ).filter(
+ vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
+ )
+ related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
+
+ paginate = {
+ 'paginator_class': EnhancedPaginator,
+ 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+ }
+ RequestConfig(request, paginate).configure(related_ips_table)
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
- # 'parent_prefixes_table': parent_prefixes_table,
- # 'duplicate_ips_table': duplicate_ips_table,
- # 'related_ips_table': related_ips_table,
+ 'parent_prefixes_table': parent_prefixes_table,
+ 'duplicate_ips_table': duplicate_ips_table,
+ 'related_ips_table': related_ips_table,
})
diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py
index 2b49dd99e..f628ca917 100644
--- a/netbox/utilities/filters.py
+++ b/netbox/utilities/filters.py
@@ -256,10 +256,6 @@ class BaseFilterSet(django_filters.FilterSet):
except django_filters.exceptions.FieldLookupError:
# The filter could not be created because the lookup expression is not supported on the field
continue
- except Exception as e:
- print(existing_filter_name, existing_filter)
- print(f'field: {field}, lookup_expr: {lookup_expr}')
- raise e
if lookup_name.startswith('n'):
# This is a negation filter which requires a queryset.exclude() clause
diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py
index 3a720e56f..d5fff805b 100644
--- a/netbox/virtualization/api/views.py
+++ b/netbox/virtualization/api/views.py
@@ -78,4 +78,4 @@ class InterfaceViewSet(ModelViewSet):
'virtual_machine', 'tags'
)
serializer_class = serializers.VMInterfaceSerializer
- filterset_class = filters.InterfaceFilterSet
+ filterset_class = filters.VMInterfaceFilterSet
diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py
index 50bde1b3f..33ca44a22 100644
--- a/netbox/virtualization/filters.py
+++ b/netbox/virtualization/filters.py
@@ -15,8 +15,8 @@ __all__ = (
'ClusterFilterSet',
'ClusterGroupFilterSet',
'ClusterTypeFilterSet',
- 'InterfaceFilterSet',
'VirtualMachineFilterSet',
+ 'VMInterfaceFilterSet',
)
@@ -201,7 +201,7 @@ class VirtualMachineFilterSet(
)
-class InterfaceFilterSet(BaseFilterSet):
+class VMInterfaceFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index ec4b28f04..1a304931c 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -571,7 +571,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
# VM interfaces
#
-class InterfaceForm(BootstrapMixin, forms.ModelForm):
+class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
@@ -643,7 +643,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
self.cleaned_data['tagged_vlans'] = []
-class InterfaceCreateForm(BootstrapMixin, forms.Form):
+class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
widget=forms.HiddenInput()
@@ -715,7 +715,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
-class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
+class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=VMInterface.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -785,7 +785,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
-class InterfaceFilterForm(forms.Form):
+class VMInterfaceFilterForm(forms.Form):
model = VMInterface
enabled = forms.NullBooleanField(
required=False,
@@ -815,7 +815,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
return ','.join(self.cleaned_data.get('tags'))
-class InterfaceBulkCreateForm(
+class VMInterfaceBulkCreateForm(
form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
VirtualMachineBulkAddComponentForm
):
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index 31ffd1ceb..bed198326 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -475,6 +475,10 @@ class VMInterface(BaseInterface):
object_data=serialize_object(self)
)
+ @property
+ def parent(self):
+ return self.virtual_machine
+
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py
index e06714e85..fd6395220 100644
--- a/netbox/virtualization/tables.py
+++ b/netbox/virtualization/tables.py
@@ -172,7 +172,7 @@ class VirtualMachineDetailTable(VirtualMachineTable):
# VM components
#
-class InterfaceTable(BaseTable):
+class VMInterfaceTable(BaseTable):
class Meta(BaseTable.Meta):
model = VMInterface
diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py
index c307d6da6..8d525f4fe 100644
--- a/netbox/virtualization/tests/test_api.py
+++ b/netbox/virtualization/tests/test_api.py
@@ -194,7 +194,7 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
# TODO: Standardize InterfaceTest (pending #4721)
-class InterfaceTest(APITestCase):
+class VMInterfaceTest(APITestCase):
def setUp(self):
diff --git a/netbox/virtualization/tests/test_filters.py b/netbox/virtualization/tests/test_filters.py
index 9fe6b61d5..821127b92 100644
--- a/netbox/virtualization/tests/test_filters.py
+++ b/netbox/virtualization/tests/test_filters.py
@@ -367,7 +367,7 @@ class VirtualMachineTestCase(TestCase):
class InterfaceTestCase(TestCase):
queryset = VMInterface.objects.all()
- filterset = InterfaceFilterSet
+ filterset = VMInterfaceFilterSet
@classmethod
def setUpTestData(cls):
diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py
index 2a8cc8ca8..772603ea1 100644
--- a/netbox/virtualization/tests/test_views.py
+++ b/netbox/virtualization/tests/test_views.py
@@ -191,11 +191,11 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
# TODO: Update base class to DeviceComponentViewTestCase
# Blocked by #4721
-class InterfaceTestCase(
+class VMInterfaceTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
- # ViewTestCases.BulkCreateObjectsViewTestCase,
+ ViewTestCases.BulkCreateObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase,
):
@@ -234,7 +234,6 @@ class InterfaceTestCase(
'virtual_machine': virtualmachines[1].pk,
'name': 'Interface X',
'enabled': False,
- 'mgmt_only': False,
'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 2000,
'description': 'New description',
@@ -248,7 +247,6 @@ class InterfaceTestCase(
'virtual_machine': virtualmachines[1].pk,
'name_pattern': 'Interface [4-6]',
'enabled': False,
- 'mgmt_only': False,
'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 2000,
'description': 'New description',
@@ -264,6 +262,6 @@ class InterfaceTestCase(
'mtu': 2000,
'description': 'New description',
'mode': InterfaceModeChoices.MODE_TAGGED,
- # 'untagged_vlan': vlans[0].pk,
- # 'tagged_vlans': [v.pk for v in vlans[1:4]],
+ 'untagged_vlan': vlans[0].pk,
+ 'tagged_vlans': [v.pk for v in vlans[1:4]],
}
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index e02103bc0..1fff399ed 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -291,9 +291,9 @@ class VirtualMachineBulkDeleteView(BulkDeleteView):
class InterfaceListView(ObjectListView):
queryset = VMInterface.objects.prefetch_related('virtual_machine', 'virtual_machine__tenant', 'cable')
- filterset = filters.InterfaceFilterSet
- filterset_form = forms.InterfaceFilterForm
- table = tables.InterfaceTable
+ filterset = filters.VMInterfaceFilterSet
+ filterset_form = forms.VMInterfaceFilterForm
+ table = tables.VMInterfaceTable
action_buttons = ('import', 'export')
@@ -334,14 +334,14 @@ class InterfaceView(ObjectView):
# TODO: This should not use ComponentCreateView
class InterfaceCreateView(ComponentCreateView):
queryset = VMInterface.objects.all()
- form = forms.InterfaceCreateForm
- model_form = forms.InterfaceForm
+ form = forms.VMInterfaceCreateForm
+ model_form = forms.VMInterfaceForm
template_name = 'virtualization/virtualmachine_component_add.html'
class InterfaceEditView(ObjectEditView):
queryset = VMInterface.objects.all()
- model_form = forms.InterfaceForm
+ model_form = forms.VMInterfaceForm
template_name = 'virtualization/vminterface_edit.html'
@@ -351,13 +351,13 @@ class InterfaceDeleteView(ObjectDeleteView):
class InterfaceBulkEditView(BulkEditView):
queryset = VMInterface.objects.all()
- table = tables.InterfaceTable
- form = forms.InterfaceBulkEditForm
+ table = tables.VMInterfaceTable
+ form = forms.VMInterfaceBulkEditForm
class InterfaceBulkDeleteView(BulkDeleteView):
queryset = VMInterface.objects.all()
- table = tables.InterfaceTable
+ table = tables.VMInterfaceTable
#
@@ -367,9 +367,9 @@ class InterfaceBulkDeleteView(BulkDeleteView):
class VirtualMachineBulkAddInterfaceView(BulkComponentCreateView):
parent_model = VirtualMachine
parent_field = 'virtual_machine'
- form = forms.InterfaceBulkCreateForm
+ form = forms.VMInterfaceBulkCreateForm
queryset = VMInterface.objects.all()
- model_form = forms.InterfaceForm
+ model_form = forms.VMInterfaceForm
filterset = filters.VirtualMachineFilterSet
table = tables.VirtualMachineTable
default_return_url = 'virtualization:virtualmachine_list'