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')
#
# 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
#
@ -1390,24 +1412,6 @@ class PowerOutletBulkEditForm(
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(
ComponentBulkEditForm,
form_from_model(Interface, [

View File

@ -696,6 +696,102 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
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
#
@ -825,98 +921,6 @@ class PowerOutletImportForm(NetBoxModelImportForm):
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):
device = CSVModelChoiceField(
label=_('Device'),

View File

@ -1202,6 +1202,24 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
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
#
@ -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):
model = Interface
fieldsets = (

View File

@ -866,6 +866,93 @@ class VCMemberSelectForm(forms.Form):
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
#
@ -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):
vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),

View File

@ -23,7 +23,6 @@ __all__ = (
'InventoryItemCreateForm',
'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm',
# 'MACAddressCreateForm',
'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
@ -239,12 +238,6 @@ class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
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 Meta(model_forms.InterfaceForm.Meta):

View File

@ -10,9 +10,9 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
from dcim.constants import *
from dcim.fields import MACAddressField, WWNField
from dcim.fields import WWNField
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.mptt import TreeManager
from utilities.ordering import naturalize_interface
@ -31,7 +31,6 @@ __all__ = (
'Interface',
'InventoryItem',
'InventoryItemRole',
'MACAddress',
'ModuleBay',
'PathEndpoint',
'PowerOutlet',
@ -1355,39 +1354,3 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
def get_status_color(self):
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 django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
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.constants import *
from dcim.fields import MACAddressField
from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet
from netbox.choices import ColorChoices
@ -33,6 +35,7 @@ __all__ = (
'Device',
'DeviceRole',
'DeviceType',
'MACAddress',
'Manufacturer',
'Module',
'ModuleType',
@ -1473,3 +1476,43 @@ class VirtualDeviceContext(PrimaryModel):
raise ValidationError({
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(
label=_('Device Components'),
label=_('Addressing'),
items=(
get_model_item('dcim', 'macaddress', _('MAC Addresses')),
),
),
MenuGroup(
label=_('Device Components'),
items=(
get_model_item('dcim', 'interface', _('Interfaces')),
get_model_item('dcim', 'frontport', _('Front Ports')),
get_model_item('dcim', 'rearport', _('Rear Ports')),