mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Designate primary MAC address via a ForeignKey on the interface models
This commit is contained in:
parent
a0db3b0719
commit
95919d1a79
@ -169,8 +169,11 @@ class MACAddressSerializer(NetBoxModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = ['mac_address', 'is_primary', 'assigned_object_type', 'assigned_object']
|
||||
brief_fields = ('mac_address',)
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object',
|
||||
'description', 'comments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'mac_address', 'description')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_assigned_object(self, obj):
|
||||
|
@ -1647,7 +1647,7 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = ('id', 'description', 'is_primary', 'assigned_object_type', 'assigned_object_id')
|
||||
fields = ('id', 'description', 'assigned_object_type', 'assigned_object_id')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1789,6 +1789,17 @@ class InterfaceFilterSet(
|
||||
field_name='mac_addresses__mac_address',
|
||||
label=_('MAC Address')
|
||||
)
|
||||
primary_mac_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
label=_('Primary MAC address (ID)'),
|
||||
)
|
||||
primary_mac_address = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address__mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
to_field_name='mac_address',
|
||||
label=_('Primary MAC address'),
|
||||
)
|
||||
wwn = MultiValueWWNFilter()
|
||||
poe_mode = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfacePoEModeChoices
|
||||
|
@ -1732,17 +1732,10 @@ class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
is_primary = forms.NullBooleanField(
|
||||
label=_('Is primary'),
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('description', 'is_primary'),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'comments',
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
@ -1203,8 +1203,7 @@ class MACAddressImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'description', 'comments', 'tags',
|
||||
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -1230,25 +1229,27 @@ class MACAddressImportForm(NetBoxModelImportForm):
|
||||
device = self.cleaned_data.get('device')
|
||||
virtual_machine = self.cleaned_data.get('virtual_machine')
|
||||
interface = self.cleaned_data.get('interface')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
|
||||
# Validate is_primary
|
||||
# Validate interface assignment
|
||||
if interface and not device and not virtual_machine:
|
||||
raise forms.ValidationError({
|
||||
"interface": _("Must specify the parent device or VM when assigning an interface")
|
||||
})
|
||||
if is_primary and not interface:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": _("No interface specified; cannot set as primary")
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set interface assignment
|
||||
if self.cleaned_data.get('interface'):
|
||||
self.instance.assigned_object = self.cleaned_data['interface']
|
||||
if interface := self.cleaned_data.get('interface'):
|
||||
self.instance.assigned_object = interface
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign the MAC address as primary for its interface, if designated as such
|
||||
if interface and self.cleaned_data['is_primary'] and self.instance.pk:
|
||||
interface.primary_mac_address = self.instance
|
||||
interface.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
#
|
||||
|
@ -3,7 +3,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import MACAddress
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import DynamicModelChoiceField
|
||||
|
||||
__all__ = (
|
||||
'InterfaceCommonForm',
|
||||
@ -18,6 +20,11 @@ class InterfaceCommonForm(forms.Form):
|
||||
max_value=INTERFACE_MTU_MAX,
|
||||
label=_('MTU')
|
||||
)
|
||||
primary_mac_address = DynamicModelChoiceField(
|
||||
queryset=MACAddress.objects.all(),
|
||||
label=_('Primary MAC address'),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -35,6 +42,10 @@ class InterfaceCommonForm(forms.Form):
|
||||
if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
|
||||
del self.fields['qinq_svlan']
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
filter_name = f'{self._meta.model._meta.model_name}_id'
|
||||
self.fields['primary_mac_address'].widget.add_query_param(filter_name, self.instance.pk)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
|
@ -1583,21 +1583,13 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('mac_address', 'is_primary', name=_('Addressing')),
|
||||
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
|
||||
FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label=_('MAC address')
|
||||
)
|
||||
is_primary = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Is primary'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
@ -1405,7 +1404,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
|
||||
),
|
||||
FieldSet('vrf', 'wwn', name=_('Addressing')),
|
||||
FieldSet('vrf', 'primary_mac_address', 'wwn', name=_('Addressing')),
|
||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
@ -1425,7 +1424,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge',
|
||||
'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
|
||||
'tags',
|
||||
]
|
||||
widgets = {
|
||||
'speed': NumberWithOptions(
|
||||
@ -1740,10 +1740,6 @@ class MACAddressForm(NetBoxModelForm):
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Primary for interface'),
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
@ -1754,14 +1750,13 @@ class MACAddressForm(NetBoxModelForm):
|
||||
FieldSet('interface', name=_('Device')),
|
||||
FieldSet('vminterface', name=_('Virtual Machine')),
|
||||
),
|
||||
'is_primary', name=_('Assignment')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'mac_address', 'interface', 'vminterface', 'is_primary', 'description', 'tags',
|
||||
'mac_address', 'interface', 'vminterface', 'description', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1790,18 +1785,6 @@ class MACAddressForm(NetBoxModelForm):
|
||||
selected_objects[1]: _("A MAC address can only be assigned to a single object.")
|
||||
})
|
||||
elif selected_objects:
|
||||
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||
if self.instance.pk and self.instance.assigned_object and self.instance.is_primary and assigned_object != self.instance.assigned_object:
|
||||
raise ValidationError(
|
||||
_("Cannot reassign MAC address while it is designated as the primary for the interface")
|
||||
)
|
||||
self.instance.assigned_object = assigned_object
|
||||
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
||||
else:
|
||||
self.instance.assigned_object = None
|
||||
|
||||
# Primary MAC address assignment is only available if an interface has been assigned.
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if self.cleaned_data.get('is_primary') and not interface:
|
||||
self.add_error(
|
||||
'is_primary', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
|
||||
)
|
||||
|
@ -390,7 +390,6 @@ class MACAddressType(NetBoxObjectType):
|
||||
)
|
||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
_name: str
|
||||
mac_address: str | None
|
||||
wwn: str | None
|
||||
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
@ -398,6 +397,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
||||
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
|
@ -24,7 +24,6 @@ class Migration(migrations.Migration):
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('mac_address', dcim.fields.MACAddressField()),
|
||||
('is_primary', models.BooleanField(default=True)),
|
||||
('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('assigned_object_type', 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')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_mac_addresses(apps, schema_editor):
|
||||
@ -9,14 +10,18 @@ def populate_mac_addresses(apps, schema_editor):
|
||||
|
||||
mac_addresses = [
|
||||
MACAddress(
|
||||
mac_address=interface._mac_address,
|
||||
mac_address=interface.mac_address,
|
||||
assigned_object_type=interface_ct,
|
||||
assigned_object_id=interface.pk
|
||||
)
|
||||
for interface in Interface.objects.filter(_mac_address__isnull=False)
|
||||
for interface in Interface.objects.filter(mac_address__isnull=False)
|
||||
]
|
||||
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
|
||||
|
||||
# TODO: Optimize interface updates
|
||||
for mac_address in mac_addresses:
|
||||
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -25,11 +30,16 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Rename mac_address field to avoid conflict with property
|
||||
migrations.RenameField(
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
old_name='mac_address',
|
||||
new_name='_mac_address',
|
||||
name='primary_mac_address',
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='dcim.macaddress'
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=populate_mac_addresses,
|
||||
@ -37,6 +47,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interface',
|
||||
name='_mac_address',
|
||||
name='mac_address',
|
||||
),
|
||||
]
|
||||
|
@ -567,6 +567,14 @@ class BaseInterface(models.Model):
|
||||
blank=True,
|
||||
verbose_name=_('VLAN Translation Policy')
|
||||
)
|
||||
primary_mac_address = models.OneToOneField(
|
||||
to='dcim.MACAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('primary MAC address')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -580,6 +588,14 @@ class BaseInterface(models.Model):
|
||||
'qinq_svlan': _("Only Q-in-Q interfaces may specify a service VLAN.")
|
||||
})
|
||||
|
||||
# Check that the primary MAC address (if any) is assigned to this interface
|
||||
if self.primary_mac_address and self.primary_mac_address.assigned_object != self:
|
||||
raise ValidationError({
|
||||
'primary_mac_address': _("MAC address {mac_address} is not assigned to this interface.").format(
|
||||
mac_address=self.primary_mac_address
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
||||
@ -606,9 +622,8 @@ class BaseInterface(models.Model):
|
||||
|
||||
@cached_property
|
||||
def mac_address(self):
|
||||
if macaddress := self.mac_addresses.order_by('-is_primary', 'mac_address').first():
|
||||
return macaddress.mac_address
|
||||
return None
|
||||
if self.primary_mac_address:
|
||||
return self.primary_mac_address.mac_address
|
||||
|
||||
|
||||
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
||||
|
@ -1499,10 +1499,6 @@ class MACAddress(PrimaryModel):
|
||||
ct_field='assigned_object_type',
|
||||
fk_field='assigned_object_id'
|
||||
)
|
||||
is_primary = models.BooleanField(
|
||||
verbose_name=_('is primary'),
|
||||
default=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('mac_address',)
|
||||
@ -1511,17 +1507,3 @@ class MACAddress(PrimaryModel):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.mac_address)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.is_primary and self.assigned_object:
|
||||
peer_macs = MACAddress.objects.exclude(pk=self.pk).filter(
|
||||
assigned_object_type=self.assigned_object_type,
|
||||
assigned_object_id=self.assigned_object_id,
|
||||
is_primary=True
|
||||
)
|
||||
if peer_macs.exists():
|
||||
raise ValidationError({
|
||||
'is_primary': _("A primary MAC address is already designated for this interface.")
|
||||
})
|
||||
|
@ -599,6 +599,10 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
verbose_name=_('Q-in-Q SVLAN'),
|
||||
linkify=True
|
||||
)
|
||||
primary_mac_address = tables.Column(
|
||||
verbose_name=_('MAC Address'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
def value_ip_addresses(self, value):
|
||||
return ",".join([str(obj.address) for obj in value.all()])
|
||||
@ -649,11 +653,11 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
model = models.Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||
'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', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'inventory_items', 'created', 'last_updated',
|
||||
'speed', 'speed_formatted', 'duplex', 'mode', 'primary_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', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
||||
'qinq_svlan', 'inventory_items', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@ -1127,10 +1131,6 @@ class MACAddressTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name=_('Parent')
|
||||
)
|
||||
is_primary = columns.BooleanColumn(
|
||||
verbose_name=_('Primary'),
|
||||
false_mark=None
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:macaddress_list'
|
||||
)
|
||||
@ -1141,7 +1141,6 @@ class MACAddressTable(NetBoxTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.MACAddress
|
||||
fields = (
|
||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'is_primary', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'is_primary')
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object')
|
||||
|
@ -5891,15 +5891,15 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
mac_addresses = (
|
||||
# Device MACs
|
||||
MACAddress(mac_address='00-00-00-01-01-01', assigned_object=interfaces[0], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-02-01-01', assigned_object=interfaces[1], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-03-01-01', assigned_object=interfaces[2], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-03-01-02', assigned_object=interfaces[2], is_primary=False),
|
||||
MACAddress(mac_address='00-00-00-01-01-01', assigned_object=interfaces[0]),
|
||||
MACAddress(mac_address='00-00-00-02-01-01', assigned_object=interfaces[1]),
|
||||
MACAddress(mac_address='00-00-00-03-01-01', assigned_object=interfaces[2]),
|
||||
MACAddress(mac_address='00-00-00-03-01-02', assigned_object=interfaces[2]),
|
||||
# VM MACs
|
||||
MACAddress(mac_address='00-00-00-04-01-01', assigned_object=vm_interfaces[0], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-05-01-01', assigned_object=vm_interfaces[1], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-06-01-01', assigned_object=vm_interfaces[2], is_primary=True),
|
||||
MACAddress(mac_address='00-00-00-06-01-02', assigned_object=vm_interfaces[2], is_primary=False),
|
||||
MACAddress(mac_address='00-00-00-04-01-01', assigned_object=vm_interfaces[0]),
|
||||
MACAddress(mac_address='00-00-00-05-01-01', assigned_object=vm_interfaces[1]),
|
||||
MACAddress(mac_address='00-00-00-06-01-01', assigned_object=vm_interfaces[2]),
|
||||
MACAddress(mac_address='00-00-00-06-01-02', assigned_object=vm_interfaces[2]),
|
||||
)
|
||||
MACAddress.objects.bulk_create(mac_addresses)
|
||||
|
||||
@ -5907,12 +5907,6 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'mac_address': ['00-00-00-01-01-01', '00-00-00-02-01-01']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_primary(self):
|
||||
params = {'is_primary': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'is_primary': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
|
@ -14,80 +14,85 @@
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Machine" %}</th>
|
||||
<td>{{ object.virtual_machine|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Enabled" %}</th>
|
||||
<td>
|
||||
{% if object.enabled %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Bridge" %}</th>
|
||||
<td>{{ object.bridge|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>{{ object.vrf|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "MTU" %}</th>
|
||||
<td>{{ object.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "MAC Address" %}</th>
|
||||
<td>
|
||||
{% if object.mac_address %}
|
||||
<span class="font-monospace">{{ object.mac_address }}</span>
|
||||
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "802.1Q Mode" %}</th>
|
||||
<td>{{ object.get_mode_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tunnel" %}</th>
|
||||
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Machine" %}</th>
|
||||
<td>{{ object.virtual_machine|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Enabled" %}</th>
|
||||
<td>
|
||||
{% if object.enabled %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Bridge" %}</th>
|
||||
<td>{{ object.bridge|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "MTU" %}</th>
|
||||
<td>{{ object.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "802.1Q Mode" %}</th>
|
||||
<td>{{ object.get_mode_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tunnel" %}</th>
|
||||
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||
{% plugin_right_page object %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Addressing" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "MAC Address" %}</th>
|
||||
<td>
|
||||
{% if object.mac_address %}
|
||||
<span class="font-monospace">{{ object.mac_address }}</span>
|
||||
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>{{ object.vrf|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CommonInterfaceFilterSet, ScopedFilterSet
|
||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from dcim.models import Device, DeviceRole, MACAddress, Platform, Region, Site, SiteGroup
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
@ -265,6 +265,17 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet):
|
||||
field_name='mac_addresses__mac_address',
|
||||
label=_('MAC address'),
|
||||
)
|
||||
primary_mac_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
label=_('Primary MAC address (ID)'),
|
||||
)
|
||||
primary_mac_address = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address__mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
to_field_name='mac_address',
|
||||
label=_('Primary MAC address'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
|
@ -358,7 +358,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
||||
FieldSet('vrf', name=_('Addressing')),
|
||||
FieldSet('vrf', 'primary_mac_address', name=_('Addressing')),
|
||||
FieldSet('mtu', 'enabled', name=_('Operation')),
|
||||
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
||||
FieldSet(
|
||||
@ -371,7 +371,8 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode', 'vlan_group',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
|
||||
'tags',
|
||||
]
|
||||
labels = {
|
||||
'mode': _('802.1Q Mode'),
|
||||
|
@ -106,6 +106,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
|
||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_mac_addresses(apps, schema_editor):
|
||||
@ -9,14 +10,18 @@ def populate_mac_addresses(apps, schema_editor):
|
||||
|
||||
mac_addresses = [
|
||||
MACAddress(
|
||||
mac_address=vminterface._mac_address,
|
||||
mac_address=vminterface.mac_address,
|
||||
assigned_object_type=vminterface_ct,
|
||||
assigned_object_id=vminterface.pk
|
||||
)
|
||||
for vminterface in VMInterface.objects.filter(_mac_address__isnull=False)
|
||||
for vminterface in VMInterface.objects.filter(mac_address__isnull=False)
|
||||
]
|
||||
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
|
||||
|
||||
# TODO: Optimize interface updates
|
||||
for mac_address in mac_addresses:
|
||||
VMInterface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -26,11 +31,16 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Rename mac_address field to avoid conflict with property
|
||||
migrations.RenameField(
|
||||
migrations.AddField(
|
||||
model_name='vminterface',
|
||||
old_name='mac_address',
|
||||
new_name='_mac_address',
|
||||
name='primary_mac_address',
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='dcim.macaddress'
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=populate_mac_addresses,
|
||||
@ -38,6 +48,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vminterface',
|
||||
name='_mac_address',
|
||||
name='mac_address',
|
||||
),
|
||||
]
|
@ -153,8 +153,8 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'created', 'last_updated',
|
||||
'vrf', 'primary_mac_address', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
||||
'tagged_vlans', 'qinq_svlan', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user