Misc cleanup

This commit is contained in:
Jeremy Stretch 2024-11-15 13:40:16 -05:00
parent 3183f376bd
commit 0575dfff85
9 changed files with 33 additions and 18 deletions

View File

@ -1,8 +1,8 @@
# MAC Addresses # MAC Addresses
A MAC address object in NetBox comprises a single physical (hardware) address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the "primary" MAC address for a given interface or VM interface. A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the "primary" MAC address for a given interface or VM interface.
Most interfaces only have a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface. However, for convenience and backward compatibiility reasons, the value of the `mac_address` field of the primary (or single) MAC address on an interface is reflected as a simple property in the interface detail page. Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface. However, for convenience and backward compatibiility reasons, the value of the `mac_address` field of the primary (or single) MAC address on an interface is reflected as a simple property in the interface detail page.
## Fields ## Fields

View File

@ -10,7 +10,6 @@ from dcim.models import (
) )
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.api.serializers_.ip import IPAddressSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
@ -216,7 +215,6 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
read_only=True read_only=True
) )
mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True) mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
ip_addresses = IPAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
class Meta: class Meta:
@ -229,7 +227,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf',
'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
'mac_addresses', 'ip_addresses', 'mac_addresses',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')

View File

@ -20,7 +20,7 @@ from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster, ClusterGroup, VMInterface from virtualization.models import Cluster, ClusterGroup, VMInterface, VirtualMachine
from vpn.models import L2VPN from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink from wireless.models import WirelessLAN, WirelessLink
@ -1659,7 +1659,7 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
devices = Device.objects.filter(**{'{}__in'.format(name): value}) devices = Device.objects.filter(**{f'{name}__in': value})
if not devices.exists(): if not devices.exists():
return queryset.none() return queryset.none()
interface_ids = [] interface_ids = []
@ -1670,7 +1670,7 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
) )
def filter_virtual_machine(self, queryset, name, value): def filter_virtual_machine(self, queryset, name, value):
virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value}) virtual_machines = VirtualMachine.objects.filter(**{f'{name}__in': value})
if not virtual_machines.exists(): if not virtual_machines.exists():
return queryset.none() return queryset.none()
interface_ids = [] interface_ids = []

View File

@ -1233,10 +1233,9 @@ class MACAddressImportForm(NetBoxModelImportForm):
is_primary = self.cleaned_data.get('is_primary') is_primary = self.cleaned_data.get('is_primary')
# Validate is_primary # Validate is_primary
# TODO: scope to interface rather than device/VM if interface and not device and not virtual_machine:
if is_primary and not device and not virtual_machine:
raise forms.ValidationError({ raise forms.ValidationError({
"is_primary": _("No device or virtual machine specified; cannot set as primary") "interface": _("Must specify the parent device or VM when assigning an interface")
}) })
if is_primary and not interface: if is_primary and not interface:
raise forms.ValidationError({ raise forms.ValidationError({

View File

@ -1583,7 +1583,7 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
model = MACAddress model = MACAddress
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('mac_address', name=_('Addressing')), FieldSet('mac_address', 'is_primary', name=_('Addressing')),
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')), FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
) )
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id') selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
@ -1591,6 +1591,13 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label=_('MAC address') label=_('MAC address')
) )
is_primary = forms.NullBooleanField(
required=False,
label=_('Is primary'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
device_id = DynamicModelMultipleChoiceField( device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,

View File

@ -369,12 +369,19 @@ class FrontPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.MACAddress, models.MACAddress,
fields='__all__', exclude=('assigned_object_type', 'assigned_object_id'),
filters=MACAddressFilter filters=MACAddressFilter
) )
class MACAddressType(NetBoxObjectType): class MACAddressType(NetBoxObjectType):
mac_address: str mac_address: str
@strawberry_django.field
def assigned_object(self) -> Annotated[Union[
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
], strawberry.union("MACAddressAssignmentType")] | None:
return self.assigned_object
@strawberry_django.type( @strawberry_django.type(
models.Interface, models.Interface,

View File

@ -606,7 +606,7 @@ class BaseInterface(models.Model):
@cached_property @cached_property
def mac_address(self): def mac_address(self):
if macaddress := self.mac_addresses.order_by('-is_primary').first(): if macaddress := self.mac_addresses.order_by('-is_primary', 'mac_address').first():
return macaddress.mac_address return macaddress.mac_address
return None return None

View File

@ -1516,7 +1516,12 @@ class MACAddress(PrimaryModel):
super().clean() super().clean()
if self.is_primary and self.assigned_object: if self.is_primary and self.assigned_object:
if self.assigned_object.mac_addresses.filter(is_primary=True).exclude(pk=self.pk).exists(): 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({ raise ValidationError({
'is_primary': _("There is already a primary MAC address for this interface.") 'is_primary': _("A primary MAC address is already designated for this interface.")
}) })

View File

@ -101,7 +101,6 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
read_only=True read_only=True
) )
mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True) mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
ip_addresses = IPAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
class Meta: class Meta:
model = VMInterface model = VMInterface
@ -109,7 +108,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated',
'count_ipaddresses', 'count_fhrp_groups', 'mac_addresses', 'ip_addresses', 'count_ipaddresses', 'count_fhrp_groups', 'mac_addresses',
] ]
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')