Reorganize MACAddress classes out of association with DeviceComponents

This commit is contained in:
Brian Tiemann 2024-11-05 15:50:25 -05:00
parent eff2225464
commit 016a5335ae
8 changed files with 274 additions and 254 deletions

View File

@ -1274,6 +1274,28 @@ class InventoryItemTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
#
# Addressing
#
class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = MACAddress
fieldsets = (
FieldSet('description'),
# FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
)
nullable_fields = (
'description', 'comments',
)
# #
# Device components # Device components
# #
@ -1390,24 +1412,6 @@ class PowerOutletBulkEditForm(
self.fields['power_port'].widget.attrs['disabled'] = True self.fields['power_port'].widget.attrs['disabled'] = True
class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = MACAddress
fieldsets = (
FieldSet('description'),
# FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
)
nullable_fields = (
'description', 'comments',
)
class InterfaceBulkEditForm( class InterfaceBulkEditForm(
ComponentBulkEditForm, ComponentBulkEditForm,
form_from_model(Interface, [ form_from_model(Interface, [

View File

@ -696,6 +696,102 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
return self.cleaned_data['replicate_components'] return self.cleaned_data['replicate_components']
#
# Addressing
#
class MACAddressImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent device of assigned interface (if any)')
)
virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent VM of assigned interface (if any)')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False,
to_field_name='name',
help_text=_('Assigned interface')
)
is_primary = forms.BooleanField(
label=_('Is primary'),
help_text=_('Make this the primary MAC for the assigned interface'),
required=False
)
class Meta:
model = MACAddress
fields = [
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary',
'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface queryset by assigned device
if data.get('device'):
self.fields['interface'].queryset = Interface.objects.filter(
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)
# Limit interface queryset by assigned device
elif data.get('virtual_machine'):
self.fields['interface'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
def clean(self):
super().clean()
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
# TODO: scope to interface rather than device/VM
if is_primary and not device and not virtual_machine:
raise forms.ValidationError({
"is_primary": _("No device or virtual machine specified; cannot set as primary MAC")
})
if is_primary and not interface:
raise forms.ValidationError({
"is_primary": _("No interface specified; cannot set as primary MAC")
})
def save(self, *args, **kwargs):
# Set interface assignment
if self.cleaned_data.get('interface'):
self.instance.assigned_object = self.cleaned_data['interface']
mac_address = super().save(*args, **kwargs)
# Set as primary for device/VM
# TODO: set as primary for interface
# if self.cleaned_data.get('is_primary'):
# parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
# if self.instance.address.version == 4:
# parent.primary_ip4 = ipaddress
# elif self.instance.address.version == 6:
# parent.primary_ip6 = ipaddress
# parent.save()
return mac_address
# #
# Device components # Device components
# #
@ -825,98 +921,6 @@ class PowerOutletImportForm(NetBoxModelImportForm):
self.fields['power_port'].queryset = PowerPort.objects.none() self.fields['power_port'].queryset = PowerPort.objects.none()
class MACAddressImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent device of assigned interface (if any)')
)
virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent VM of assigned interface (if any)')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False,
to_field_name='name',
help_text=_('Assigned interface')
)
is_primary = forms.BooleanField(
label=_('Is primary'),
help_text=_('Make this the primary MAC for the assigned interface'),
required=False
)
class Meta:
model = MACAddress
fields = [
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary',
'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface queryset by assigned device
if data.get('device'):
self.fields['interface'].queryset = Interface.objects.filter(
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)
# Limit interface queryset by assigned device
elif data.get('virtual_machine'):
self.fields['interface'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
def clean(self):
super().clean()
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
# TODO: scope to interface rather than device/VM
if is_primary and not device and not virtual_machine:
raise forms.ValidationError({
"is_primary": _("No device or virtual machine specified; cannot set as primary MAC")
})
if is_primary and not interface:
raise forms.ValidationError({
"is_primary": _("No interface specified; cannot set as primary MAC")
})
def save(self, *args, **kwargs):
# Set interface assignment
if self.cleaned_data.get('interface'):
self.instance.assigned_object = self.cleaned_data['interface']
mac_address = super().save(*args, **kwargs)
# Set as primary for device/VM
# TODO: set as primary for interface
# if self.cleaned_data.get('is_primary'):
# parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
# if self.instance.address.version == 4:
# parent.primary_ip4 = ipaddress
# elif self.instance.address.version == 6:
# parent.primary_ip6 = ipaddress
# parent.save()
return mac_address
class InterfaceImportForm(NetBoxModelImportForm): class InterfaceImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'), label=_('Device'),

View File

@ -1202,6 +1202,24 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
#
# Addressing
#
class MACAddressFilterForm(NetBoxModelFilterSetForm):
model = MACAddress
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('mac_address', name=_('Addressing')),
)
selector_fields = ('filter_id', 'q', 'device_id')
mac_address = forms.CharField(
required=False,
label=_('MAC address')
)
tag = TagFilterField(model)
# #
# Device components # Device components
# #
@ -1325,20 +1343,6 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
) )
class MACAddressFilterForm(NetBoxModelFilterSetForm):
model = MACAddress
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('mac_address', name=_('Addressing')),
)
selector_fields = ('filter_id', 'q', 'device_id')
mac_address = forms.CharField(
required=False,
label=_('MAC address')
)
tag = TagFilterField(model)
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface model = Interface
fieldsets = ( fieldsets = (

View File

@ -866,6 +866,93 @@ class VCMemberSelectForm(forms.Form):
return device return device
#
# Addressing
#
class MACAddressForm(NetBoxModelForm):
mac_address = forms.CharField(
required=True,
label=_('MAC address')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
)
vminterface = DynamicModelChoiceField(
label=_('VM Interface'),
queryset=VMInterface.objects.all(),
required=False,
)
is_primary = forms.BooleanField(
required=False,
label=_('Primary for interface'),
)
fieldsets = (
FieldSet(
'mac_address', 'description', 'tags',
),
FieldSet(
TabbedGroups(
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',
]
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['vminterface'] = instance.assigned_object
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
# Handle object assignment
selected_objects = [
field for field in ('interface', 'vminterface') if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError({
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.cleaned_data['is_primary'] and assigned_object != self.instance.assigned_object:
raise ValidationError(
_("Cannot reassign MAC address while it is designated as the primary MAC for the interface")
)
self.instance.assigned_object = assigned_object
else:
self.instance.assigned_object = None
# Primary MAC 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.")
)
# #
# Device component templates # Device component templates
# #
@ -1301,89 +1388,6 @@ class PowerOutletForm(ModularDeviceComponentForm):
] ]
class MACAddressForm(NetBoxModelForm):
mac_address = forms.CharField(
required=True,
label=_('MAC address')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
)
vminterface = DynamicModelChoiceField(
label=_('VM Interface'),
queryset=VMInterface.objects.all(),
required=False,
)
is_primary = forms.BooleanField(
required=False,
label=_('Primary for interface'),
)
fieldsets = (
FieldSet(
'mac_address', 'description', 'tags',
),
FieldSet(
TabbedGroups(
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',
]
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['vminterface'] = instance.assigned_object
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
# Handle object assignment
selected_objects = [
field for field in ('interface', 'vminterface') if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError({
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.cleaned_data['is_primary'] and assigned_object != self.instance.assigned_object:
raise ValidationError(
_("Cannot reassign MAC address while it is designated as the primary MAC for the interface")
)
self.instance.assigned_object = assigned_object
else:
self.instance.assigned_object = None
# Primary MAC 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.")
)
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vdcs = DynamicModelMultipleChoiceField( vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),

View File

@ -23,7 +23,6 @@ __all__ = (
'InventoryItemCreateForm', 'InventoryItemCreateForm',
'InventoryItemTemplateCreateForm', 'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm', 'ModuleBayCreateForm',
# 'MACAddressCreateForm',
'ModuleBayTemplateCreateForm', 'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm', 'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm', 'PowerOutletTemplateCreateForm',
@ -239,12 +238,6 @@ class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
exclude = ('name', 'label') exclude = ('name', 'label')
# class MACAddressCreateForm(ComponentCreateForm, model_forms.MACAddressForm):
#
# class Meta(model_forms.MACAddressForm.Meta):
# exclude = ('name', 'label')
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm): class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
class Meta(model_forms.InterfaceForm.Meta): class Meta(model_forms.InterfaceForm.Meta):

View File

@ -10,9 +10,9 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import MACAddressField, WWNField from dcim.fields import WWNField
from netbox.choices import ColorChoices from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel, PrimaryModel from netbox.models import OrganizationalModel, NetBoxModel
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
@ -31,7 +31,6 @@ __all__ = (
'Interface', 'Interface',
'InventoryItem', 'InventoryItem',
'InventoryItemRole', 'InventoryItemRole',
'MACAddress',
'ModuleBay', 'ModuleBay',
'PathEndpoint', 'PathEndpoint',
'PowerOutlet', 'PowerOutlet',
@ -1355,39 +1354,3 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
def get_status_color(self): def get_status_color(self):
return InventoryItemStatusChoices.colors.get(self.status) return InventoryItemStatusChoices.colors.get(self.status)
class MACAddress(PrimaryModel):
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name=_('MAC address')
)
assigned_object_type = models.ForeignKey(
to='contenttypes.ContentType',
limit_choices_to=MACADDRESS_ASSIGNMENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
blank=True,
null=True
)
assigned_object_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
assigned_object = GenericForeignKey(
ct_field='assigned_object_type',
fk_field='assigned_object_id'
)
is_primary = models.BooleanField(
verbose_name=_('is primary for interface'),
default=False
)
class Meta:
ordering = ('mac_address',)
verbose_name = _('MAC address')
verbose_name_plural = _('MAC addresses')
def __str__(self):
return f'{str(self.mac_address)} {self.assigned_object}'

View File

@ -3,6 +3,7 @@ import yaml
from functools import cached_property from functools import cached_property
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -16,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import MACAddressField
from extras.models import ConfigContextModel, CustomField from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet from extras.querysets import ConfigContextModelQuerySet
from netbox.choices import ColorChoices from netbox.choices import ColorChoices
@ -33,6 +35,7 @@ __all__ = (
'Device', 'Device',
'DeviceRole', 'DeviceRole',
'DeviceType', 'DeviceType',
'MACAddress',
'Manufacturer', 'Manufacturer',
'Module', 'Module',
'ModuleType', 'ModuleType',
@ -1473,3 +1476,43 @@ class VirtualDeviceContext(PrimaryModel):
raise ValidationError({ raise ValidationError({
f'primary_ip{family}': _('Primary IP address must belong to an interface on the assigned device.') f'primary_ip{family}': _('Primary IP address must belong to an interface on the assigned device.')
}) })
#
# Addressing
#
class MACAddress(PrimaryModel):
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name=_('MAC address')
)
assigned_object_type = models.ForeignKey(
to='contenttypes.ContentType',
limit_choices_to=MACADDRESS_ASSIGNMENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
blank=True,
null=True
)
assigned_object_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
assigned_object = GenericForeignKey(
ct_field='assigned_object_type',
fk_field='assigned_object_id'
)
is_primary = models.BooleanField(
verbose_name=_('is primary for interface'),
default=False
)
class Meta:
ordering = ('mac_address',)
verbose_name = _('MAC address')
verbose_name_plural = _('MAC addresses')
def __str__(self):
return f'{str(self.mac_address)} {self.assigned_object}'

View File

@ -89,9 +89,14 @@ DEVICES_MENU = Menu(
), ),
), ),
MenuGroup( MenuGroup(
label=_('Device Components'), label=_('Addressing'),
items=( items=(
get_model_item('dcim', 'macaddress', _('MAC Addresses')), get_model_item('dcim', 'macaddress', _('MAC Addresses')),
),
),
MenuGroup(
label=_('Device Components'),
items=(
get_model_item('dcim', 'interface', _('Interfaces')), get_model_item('dcim', 'interface', _('Interfaces')),
get_model_item('dcim', 'frontport', _('Front Ports')), get_model_item('dcim', 'frontport', _('Front Ports')),
get_model_item('dcim', 'rearport', _('Rear Ports')), get_model_item('dcim', 'rearport', _('Rear Ports')),