Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4867 multiple mac addresses #17902

Open
wants to merge 54 commits into
base: feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
44b6270
Create MACAddress model and migrations to convert existing .mac_addre…
bctiemann Oct 28, 2024
e0b1c1a
Add migrations
bctiemann Oct 29, 2024
eed926c
Merge branch 'feature' into 4867-multiple-mac-addresses
bctiemann Oct 29, 2024
b8572dc
All views/filtering working and documentation done; no unit tests yet
bctiemann Oct 30, 2024
5849f8d
Merge branch 'feature' into 4867-multiple-mac-addresses
bctiemann Oct 30, 2024
03bc2f3
Redo migrations following VLAN Translation
bctiemann Oct 30, 2024
eec59cf
Remove mac_address filter fields and add table columns for device/vm
bctiemann Oct 31, 2024
0fc5157
Remove unnecessary "bulk rename"
bctiemann Oct 31, 2024
de4cafe
Fix filterset tests for Device
bctiemann Oct 31, 2024
f64c941
Fix filterset tests for Interface
bctiemann Oct 31, 2024
dfaa354
Fix tests on single-object forms
bctiemann Oct 31, 2024
a631b69
Fix serializer tests
bctiemann Oct 31, 2024
5be2092
Fix filterset tests for VMInterface
bctiemann Oct 31, 2024
8e27d2b
Fix filterset tests for Device and VirtualMachine
bctiemann Oct 31, 2024
8d91407
Move new field check into lookup_map iteration
bctiemann Oct 31, 2024
a3eb80b
Fix general MACAddress filter tests
bctiemann Oct 31, 2024
2536f94
Add GraphQL types/filters/schema
bctiemann Oct 31, 2024
2d767b6
Fix bulk edit/create tests (bulk editing Interfaces will be unsupport…
bctiemann Nov 1, 2024
9206054
Make mac_address read_only on InterfaceSerializer/VMInterfaceSerializer
bctiemann Nov 1, 2024
6d2a0a4
Merge feature and rebuild migrations
bctiemann Nov 1, 2024
6231516
Merge feature and rebuild migrations
bctiemann Nov 1, 2024
e763207
Undo unrelated work
bctiemann Nov 1, 2024
1b42b90
Cleanup unused IPAddress derived stuff
bctiemann Nov 1, 2024
4f660fa
API endpoints
bctiemann Nov 4, 2024
5ab8c4c
Add serializer objects to interface serializers
bctiemann Nov 4, 2024
3fd3bdb
Clean up unnecessary bulk create forms/views/routes
bctiemann Nov 5, 2024
6d604e2
Merge branch 'feature' into 4867-multiple-mac-addresses
bctiemann Nov 5, 2024
eff2225
Add SearchIndex and adjust indexable fields for Interface and VMInter…
bctiemann Nov 5, 2024
016a533
Reorganize MACAddress classes out of association with DeviceComponents
bctiemann Nov 5, 2024
5913ae0
Move MACAddressSerializer
bctiemann Nov 5, 2024
3f32c5b
Merge branch 'feature' into 4867-multiple-mac-addresses
bctiemann Nov 13, 2024
3a7a4f8
Enforce saving only a single is_primary MACAddress per interface/vmin…
bctiemann Nov 13, 2024
5029dd4
Perform is_primary validation on MACAddress model and just check if o…
bctiemann Nov 14, 2024
640a325
Remove form-level validation
bctiemann Nov 14, 2024
90cf71b
Fix check for current is_primary setting when reassigning
bctiemann Nov 14, 2024
e46d9a6
Model cleanup
bctiemann Nov 14, 2024
e2e5af5
Documentation notes and cleanup
bctiemann Nov 14, 2024
17a2b53
Simplify serializer and add ip_addresses
bctiemann Nov 14, 2024
563caa9
Add to VMInterfaceSerializer too
bctiemann Nov 14, 2024
3d244bf
Style cleanup
bctiemann Nov 14, 2024
944eada
Standardize "MAC Address" instead of "MAC"
bctiemann Nov 14, 2024
57bc188
Remove unused views
bctiemann Nov 14, 2024
b066330
Add is_primary field for bulk edit
bctiemann Nov 14, 2024
4655372
HTML cleanup and add copy-to-clipboard button
bctiemann Nov 14, 2024
eddf0e4
Remove mac_address from Interface and VMInterface bulk-edit forms
bctiemann Nov 14, 2024
c7fd292
Add device and VM filtering
bctiemann Nov 14, 2024
8c8ec6b
Use combined assigned_object_parent in table to match structure of IP…
bctiemann Nov 14, 2024
fc4483d
Add GFK fields to MACAddressSerializer
bctiemann Nov 15, 2024
0214b9e
Reorganize "Addressing" sections to remove from proximity to "Device …
bctiemann Nov 15, 2024
1a10052
Merge branch 'feature' into 4867-multiple-mac-addresses
jeremystretch Nov 15, 2024
3183f37
Clean up migrations
jeremystretch Nov 15, 2024
0575dff
Misc cleanup
jeremystretch Nov 15, 2024
e4530bc
Add filterset test
jeremystretch Nov 15, 2024
a0db3b0
Remove mac_address field from interface forms
jeremystretch Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions docs/models/dcim/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Interfaces in NetBox represent network interfaces used to exchange data with con

## Fields

!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.

### Device

The device to which this interface belongs.
Expand Down Expand Up @@ -45,10 +48,6 @@ The operation duplex (full, half, or auto).

The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.

### MAC Address

The 48-bit MAC address (for Ethernet interfaces).

### WWN

The 64-bit world-wide name (for Fibre Channel interfaces).
Expand Down
11 changes: 11 additions & 0 deletions docs/models/dcim/macaddress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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.
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved

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.

## Fields

### MAC Address

The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).
7 changes: 3 additions & 4 deletions docs/models/virtualization/vminterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

## Fields

!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.

### Virtual Machine

The [virtual machine](./virtualmachine.md) to which this interface is assigned.
Expand All @@ -27,10 +30,6 @@ An interface on the same VM with which this interface is bridged.

If not selected, this interface will be treated as disabled/inoperative.

### MAC Address

The 48-bit MAC address (for Ethernet interfaces).

### MTU

The interface's configured maximum transmissible unit (MTU).
Expand Down
12 changes: 7 additions & 5 deletions netbox/dcim/api/serializers_/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.api.serializers_.ip import IPAddressSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
Expand All @@ -21,7 +22,7 @@
from wireless.models import WirelessLAN
from .base import ConnectedEndpointsSerializer
from .cables import CabledObjectSerializer
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
from .manufacturers import ManufacturerSerializer
from .nested import NestedInterfaceSerializer
from .roles import InventoryItemRoleSerializer
Expand Down Expand Up @@ -211,11 +212,11 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
count_ipaddresses = serializers.IntegerField(read_only=True)
count_fhrp_groups = serializers.IntegerField(read_only=True)
mac_address = serializers.CharField(
required=False,
default=None,
allow_blank=True,
allow_null=True
allow_null=True,
read_only=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)
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)

class Meta:
Expand All @@ -228,6 +229,7 @@ class Meta:
'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf',
'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
'mac_addresses', 'ip_addresses',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')

Expand Down
30 changes: 28 additions & 2 deletions netbox/dcim/api/serializers_/devices.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import decimal

from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from dcim.choices import *
from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext
from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS
from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from ipam.api.serializers_.ip import IPAddressSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.serializers_.clusters import ClusterSerializer
from .devicetypes import *
from .platforms import PlatformSerializer
Expand All @@ -23,6 +26,7 @@
__all__ = (
'DeviceSerializer',
'DeviceWithConfigContextSerializer',
'MACAddressSerializer',
'ModuleSerializer',
'VirtualDeviceContextSerializer',
)
Expand Down Expand Up @@ -153,3 +157,25 @@ class Meta:
'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')


class MACAddressSerializer(NetBoxModelSerializer):
bctiemann marked this conversation as resolved.
Show resolved Hide resolved
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
required=False,
allow_null=True
)
assigned_object = serializers.SerializerMethodField(read_only=True)

class Meta:
model = MACAddress
fields = ['mac_address', 'is_primary', 'assigned_object_type', 'assigned_object']
brief_fields = ('mac_address',)

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, obj):
if obj.assigned_object is None:
return None
serializer = get_serializer_for_model(obj.assigned_object)
context = {'request': self.context['request']}
return serializer(obj.assigned_object, nested=True, context=context).data
3 changes: 3 additions & 0 deletions netbox/dcim/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
# Device component roles
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)

# Addressing
router.register('mac-addresses', views.MACAddressViewSet)

# Cables
router.register('cables', views.CableViewSet)
router.register('cable-terminations', views.CableTerminationViewSet)
Expand Down
10 changes: 10 additions & 0 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,16 @@ class InventoryItemRoleViewSet(NetBoxModelViewSet):
filterset_class = filtersets.InventoryItemRoleFilterSet


#
# Addressing
#

class MACAddressViewSet(NetBoxModelViewSet):
queryset = MACAddress.objects.all()
serializer_class = serializers.MACAddressSerializer
filterset_class = filtersets.MACAddressFilterSet


#
# Cables
#
Expand Down
10 changes: 10 additions & 0 deletions netbox/dcim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,13 @@
LOCATION_SCOPE_TYPES = (
'region', 'sitegroup', 'site', 'location',
)


#
# MAC addresses
#

MACADDRESS_ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='interface') |
Q(app_label='virtualization', model='vminterface')
)
91 changes: 88 additions & 3 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster, ClusterGroup
from virtualization.models import Cluster, ClusterGroup, VMInterface
from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
Expand Down Expand Up @@ -52,6 +52,7 @@
'InventoryItemRoleFilterSet',
'InventoryItemTemplateFilterSet',
'LocationFilterSet',
'MACAddressFilterSet',
'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
Expand Down Expand Up @@ -1100,7 +1101,7 @@ class DeviceFilterSet(
label=_('Is full depth'),
)
mac_address = MultiValueMACAddressFilter(
field_name='interfaces__mac_address',
field_name='interfaces__mac_addresses__mac_address',
label=_('MAC address'),
)
serial = MultiValueCharFilter(
Expand Down Expand Up @@ -1599,6 +1600,87 @@ class Meta:
)


class MACAddressFilterSet(NetBoxModelFilterSet):
bctiemann marked this conversation as resolved.
Show resolved Hide resolved
mac_address = MultiValueMACAddressFilter()
device = MultiValueCharFilter(
method='filter_device',
field_name='name',
label=_('Device (name)'),
)
device_id = MultiValueNumberFilter(
method='filter_device',
field_name='pk',
label=_('Device (ID)'),
)
virtual_machine = MultiValueCharFilter(
method='filter_virtual_machine',
field_name='name',
label=_('Virtual machine (name)'),
)
virtual_machine_id = MultiValueNumberFilter(
method='filter_virtual_machine',
field_name='pk',
label=_('Virtual machine (ID)'),
)
interface = django_filters.ModelMultipleChoiceFilter(
field_name='interface__name',
queryset=Interface.objects.all(),
to_field_name='name',
label=_('Interface (name)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
field_name='interface',
queryset=Interface.objects.all(),
label=_('Interface (ID)'),
)
vminterface = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__name',
queryset=VMInterface.objects.all(),
to_field_name='name',
label=_('VM interface (name)'),
)
vminterface_id = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface',
queryset=VMInterface.objects.all(),
label=_('VM interface (ID)'),
)

class Meta:
model = MACAddress
fields = ('id', 'description', 'is_primary', 'assigned_object_type', 'assigned_object_id')

def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(mac_address__icontains=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)

def filter_device(self, queryset, name, value):
devices = Device.objects.filter(**{'{}__in'.format(name): value})
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved
if not devices.exists():
return queryset.none()
interface_ids = []
for device in devices:
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
return queryset.filter(
interface__in=interface_ids
)

def filter_virtual_machine(self, queryset, name, value):
virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value})
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved
if not virtual_machines.exists():
return queryset.none()
interface_ids = []
for vm in virtual_machines:
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
return queryset.filter(
vminterface__in=interface_ids
)


class CommonInterfaceFilterSet(django_filters.FilterSet):
vlan_id = django_filters.CharFilter(
method='filter_vlan_id',
Expand Down Expand Up @@ -1703,7 +1785,10 @@ class InterfaceFilterSet(
duplex = django_filters.MultipleChoiceFilter(
choices=InterfaceDuplexChoices
)
mac_address = MultiValueMACAddressFilter()
mac_address = MultiValueMACAddressFilter(
field_name='mac_addresses__mac_address',
label=_('MAC Address')
)
wwn = MultiValueWWNFilter()
poe_mode = django_filters.MultipleChoiceFilter(
choices=InterfacePoEModeChoices
Expand Down
31 changes: 29 additions & 2 deletions netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'InventoryItemRoleBulkEditForm',
'InventoryItemTemplateBulkEditForm',
'LocationBulkEditForm',
'MACAddressBulkEditForm',
'ManufacturerBulkEditForm',
'ModuleBulkEditForm',
'ModuleBayBulkEditForm',
Expand Down Expand Up @@ -1392,7 +1393,7 @@ def __init__(self, *args, **kwargs):
class InterfaceBulkEditForm(
ComponentBulkEditForm,
form_from_model(Interface, [
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tx_power', 'wireless_lans'
])
Expand Down Expand Up @@ -1506,7 +1507,7 @@ class InterfaceBulkEditForm(
model = Interface
fieldsets = (
FieldSet('module', 'type', 'label', 'speed', 'duplex', 'description'),
FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
FieldSet('vrf', 'wwn', name=_('Addressing')),
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
Expand Down Expand Up @@ -1719,3 +1720,29 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('device', 'status', 'tenant'),
)
nullable_fields = ('device', 'tenant', )


#
# Addressing
#

class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
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',
)
Loading