From 4aaa1f6606101e6a1f209030bb3a9b75a4fa4835 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Fri, 6 Jun 2025 22:52:09 +0200 Subject: [PATCH 1/3] feat(wireless): Allow Link import of interface by name Enhances the WirelessLink bulk import form to support specifying site and device for both terminations (A and B), with dynamic queryset filtering based on selection. Also allows interfaces to be referenced by name (instead of ID) during import, improving usability for CSV workflows. --- netbox/wireless/forms/bulk_import.py | 75 ++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 389dcf25d30..29395f81483 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -2,7 +2,7 @@ from dcim.choices import LinkStatusChoices from dcim.forms.mixins import ScopedImportForm -from dcim.models import Interface +from dcim.models import Device, Interface, Site from ipam.models import VLAN from netbox.choices import * from netbox.forms import NetBoxModelImportForm @@ -85,18 +85,53 @@ class Meta: class WirelessLinkImportForm(NetBoxModelImportForm): - status = CSVChoiceField( - label=_('Status'), - choices=LinkStatusChoices, - help_text=_('Connection status') + # Termination A + site_a = CSVModelChoiceField( + label=_('Site A'), + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text=_('Site of parent device A (if any)'), + ) + device_a = CSVModelChoiceField( + label=_('Device A'), + queryset=Device.objects.all(), + to_field_name='name', + help_text=_('Parent device of assigned interface A'), ) interface_a = CSVModelChoiceField( label=_('Interface A'), - queryset=Interface.objects.all() + queryset=Interface.objects.all(), + to_field_name='name', + help_text=_('Assigned interface A'), + ) + + # Termination B + site_b = CSVModelChoiceField( + label=_('Site B'), + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text=_('Site of parent device B (if any)'), + ) + device_b = CSVModelChoiceField( + label=_('Device B'), + queryset=Device.objects.all(), + to_field_name='name', + help_text=_('Parent device of assigned interface B'), ) interface_b = CSVModelChoiceField( label=_('Interface B'), - queryset=Interface.objects.all() + queryset=Interface.objects.all(), + to_field_name='name', + help_text=_('Assigned interface B'), + ) + + # WirelessLink attributes + status = CSVChoiceField( + label=_('Status'), + choices=LinkStatusChoices, + help_text=_('Connection status'), ) tenant = CSVModelChoiceField( label=_('Tenant'), @@ -127,6 +162,28 @@ class WirelessLinkImportForm(NetBoxModelImportForm): class Meta: model = WirelessLink fields = ( - 'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'distance', 'distance_unit', 'description', 'comments', 'tags', + 'site_a', 'device_a', 'interface_a', 'site_b', 'device_b', 'interface_b', 'status', 'ssid', 'tenant', + 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'comments', 'tags', ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + # Limit choices for interface_a to the assigned device_a + interface_a_params = {f'device__{self.fields["device_a"].to_field_name}': data.get('device_a')} + # Limit choices for device_a to the assigned site_a + if site_a := data.get('site_a'): + device_a_params = {f'site__{self.fields["site_a"].to_field_name}': site_a} + self.fields['device_a'].queryset = self.fields['device_a'].queryset.filter(**device_a_params) + interface_a_params.update({f'device__site__{self.fields["site_a"].to_field_name}': site_a}) + self.fields['interface_a'].queryset = self.fields['interface_a'].queryset.filter(**interface_a_params) + + # Limit choices for interface_b to the assigned device_b + interface_b_params = {f'device__{self.fields["device_b"].to_field_name}': data.get('device_b')} + # Limit choices for device_b to the assigned site_b + if site_b := data.get('site_b'): + device_b_params = {f'site__{self.fields["site_b"].to_field_name}': site_b} + self.fields['device_b'].queryset = self.fields['device_b'].queryset.filter(**device_b_params) + interface_b_params.update({f'device__site__{self.fields["site_b"].to_field_name}': site_b}) + self.fields['interface_b'].queryset = self.fields['interface_b'].queryset.filter(**interface_b_params) From a021d292800886fa54aef3baf38c07a53f047bed Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Fri, 6 Jun 2025 23:02:25 +0200 Subject: [PATCH 2/3] feat(wireless): Update CSV format for WirelessLink tests Modifies the CSV data structure in WirelessLink test cases to include device names alongside interfaces for both terminations. Enhances readability and aligns tests with import functionality updates. --- netbox/wireless/tests/test_views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/wireless/tests/test_views.py b/netbox/wireless/tests/test_views.py index 975f18c0d5a..587ae7f8926 100644 --- a/netbox/wireless/tests/test_views.py +++ b/netbox/wireless/tests/test_views.py @@ -198,10 +198,10 @@ def setUpTestData(cls): } cls.csv_data = ( - "interface_a,interface_b,status,tenant", - f"{interfaces[6].pk},{interfaces[7].pk},connected,{tenants[0].name}", - f"{interfaces[8].pk},{interfaces[9].pk},connected,{tenants[1].name}", - f"{interfaces[10].pk},{interfaces[11].pk},connected,{tenants[2].name}", + "device_a,interface_a,device_b,interface_b,status,tenant", + f"{interfaces[6].device.name},{interfaces[6].name},{interfaces[7].device.name},{interfaces[7].name},connected,{tenants[0].name}", + f"{interfaces[8].device.name},{interfaces[8].name},{interfaces[9].device.name},{interfaces[9].name},connected,{tenants[1].name}", + f"{interfaces[10].device.name},{interfaces[10].name},{interfaces[11].device.name},{interfaces[11].name},connected,{tenants[2].name}", ) cls.csv_update_data = ( From 5114424d88b129f17b17cddee0c7bc815afaef98 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Wed, 11 Jun 2025 16:07:12 +0200 Subject: [PATCH 3/3] fix(wireless): Add null checks for WirelessLink interface validation Adds checks for the presence of `interface_a` and `interface_b` to avoid attribute errors during WirelessLink validation. Ensures robust handling of edge cases where the attributes may be missing. --- netbox/wireless/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 9c73ae5b759..11f9e06eb7a 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -198,13 +198,13 @@ def clean(self): super().clean() # Validate interface types - if self.interface_a.type not in WIRELESS_IFACE_TYPES: + if hasattr(self, "interface_a") and self.interface_a.type not in WIRELESS_IFACE_TYPES: raise ValidationError({ 'interface_a': _( "{type} is not a wireless interface." ).format(type=self.interface_a.get_type_display()) }) - if self.interface_b.type not in WIRELESS_IFACE_TYPES: + if hasattr(self, "interface_b") and self.interface_b.type not in WIRELESS_IFACE_TYPES: raise ValidationError({ 'interface_b': _( "{type} is not a wireless interface."