diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 989726edb..fc3740374 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -42,7 +42,6 @@ router.register('virtual-device-contexts', views.VirtualDeviceContextViewSet) router.register('modules', views.ModuleViewSet) # Device components -router.register('mac-addresses', views.MACAddressViewSet) router.register('console-ports', views.ConsolePortViewSet) router.register('console-server-ports', views.ConsoleServerPortViewSet) router.register('power-ports', views.PowerPortViewSet) @@ -57,6 +56,9 @@ router.register('inventory-items', views.InventoryItemViewSet) # 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) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 2885216b3..d7dbbef91 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -408,12 +408,6 @@ class ModuleViewSet(NetBoxModelViewSet): # Device components # -class MACAddressViewSet(NetBoxModelViewSet): - queryset = MACAddress.objects.all() - serializer_class = serializers.MACAddressSerializer - filterset_class = filtersets.MACAddressFilterSet - - class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = ConsolePort.objects.prefetch_related( '_path', 'cable__terminations', @@ -505,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 # diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index a5405522d..a2fa28eaf 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1274,32 +1274,6 @@ 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 - ) - is_primary = forms.NullBooleanField( - label=_('Is primary'), - required=False, - widget=BulkEditNullBooleanSelect(), - ) - comments = CommentField() - - model = MACAddress - fieldsets = ( - FieldSet('description', 'is_primary'), - ) - nullable_fields = ( - 'description', 'comments', - ) - - # # Device components # @@ -1746,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', + ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index b049584ce..33713e9b2 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -696,90 +696,6 @@ 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 address 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") - }) - 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'] - - return super().save(*args, **kwargs) - - # # Device components # @@ -1252,6 +1168,90 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm): fields = ('name', 'slug', 'color', 'description') +# +# 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 address 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") + }) + 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'] + + return super().save(*args, **kwargs) + + # # Cables # diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 0b7c11db7..6ce747d1f 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1202,35 +1202,6 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -# -# Addressing -# - -class MACAddressFilterForm(NetBoxModelFilterSetForm): - model = MACAddress - fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), - FieldSet('mac_address', name=_('Addressing')), - FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')), - ) - selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id') - mac_address = forms.CharField( - required=False, - label=_('MAC address') - ) - device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - label=_('Assigned Device'), - ) - virtual_machine_id = DynamicModelMultipleChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - label=_('Assigned VM'), - ) - tag = TagFilterField(model) - - # # Device components # @@ -1604,6 +1575,35 @@ class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) +# +# Addressing +# + +class MACAddressFilterForm(NetBoxModelFilterSetForm): + model = MACAddress + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('mac_address', name=_('Addressing')), + FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')), + ) + selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id') + mac_address = forms.CharField( + required=False, + label=_('MAC address') + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + label=_('Assigned Device'), + ) + virtual_machine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + label=_('Assigned VM'), + ) + tag = TagFilterField(model) + + # # Connections # diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 00cf8272a..f62a02921 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -866,92 +866,6 @@ 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.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 - 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.") - ) - - # # Device component templates # @@ -1820,3 +1734,89 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags' ] + + +# +# 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.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 + 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.") + )