15106 Add Length Field to Wireless Link (#16528)

* 15106 add wireles link length

* 15106 add wireles link length

* 15106 add wireless link length

* 15106 add tests

* 15106 rename length -> distance

* 15106 rename length -> distance

* 15106 review comments

* 15106 review comments

* 15106 fix form

* 15106 length -> distance
This commit is contained in:
Arthur Hanson 2024-06-17 06:19:49 -07:00 committed by GitHub
parent e12edd7420
commit 91dcecbd07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 168 additions and 17 deletions

View File

@ -40,3 +40,7 @@ The security cipher used to apply wireless authentication. Options include:
### Pre-Shared Key ### Pre-Shared Key
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types. The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
### Distance
The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet).

View File

@ -656,11 +656,6 @@ class CableForm(TenancyForm, NetBoxModelForm):
'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
'length', 'length_unit', 'description', 'comments', 'tags', 'length', 'length_unit', 'description', 'comments', 'tags',
] ]
error_messages = {
'length': {
'max_value': _('Maximum length is 32767 (any unit)')
}
}
class PowerPanelForm(NetBoxModelForm): class PowerPanelForm(NetBoxModelForm):

View File

@ -393,6 +393,8 @@ class CableTraceSVG:
labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()] labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()]
if cable.ssid: if cable.ssid:
description.append(f"{cable.ssid}") description.append(f"{cable.ssid}")
if cable.distance and cable.distance_unit:
description.append(f"{cable.distance} {cable.get_distance_unit_display()}")
near = [term for term in near_terminations if term.object == cable.interface_a] near = [term for term in near_terminations if term.object == cable.interface_a]
far = [term for term in far_terminations if term.object == cable.interface_b] far = [term for term in far_terminations if term.object == cable.interface_b]
if not (near and far): if not (near and far):

View File

@ -109,7 +109,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
length = columns.TemplateColumn( length = columns.TemplateColumn(
template_code=CABLE_LENGTH, template_code=CABLE_LENGTH,
order_by=('_abs_length', 'length_unit') order_by=('_abs_length')
) )
color = columns.ColorColumn() color = columns.ColorColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()

View File

@ -34,6 +34,16 @@
<th scope="row">{% trans "Description" %}</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Distance" %}</th>
<td>
{% if object.distance is not None %}
{{ object.distance|floatformat }} {{ object.get_distance_unit_display }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table> </table>
</div> </div>
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}

View File

@ -21,11 +21,13 @@ class WirelessLinkSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
distance_unit = ChoiceField(choices=WirelessLinkDistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta: class Meta:
model = WirelessLink model = WirelessLink
fields = [ fields = [
'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type',
'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'ssid', 'description') brief_fields = ('id', 'url', 'display', 'ssid', 'description')

View File

@ -481,3 +481,21 @@ class WirelessAuthCipherChoices(ChoiceSet):
(CIPHER_TKIP, 'TKIP'), (CIPHER_TKIP, 'TKIP'),
(CIPHER_AES, 'AES'), (CIPHER_AES, 'AES'),
) )
class WirelessLinkDistanceUnitChoices(ChoiceSet):
# Metric
UNIT_KILOMETER = 'km'
UNIT_METER = 'm'
# Imperial
UNIT_MILE = 'mi'
UNIT_FOOT = 'ft'
CHOICES = (
(UNIT_KILOMETER, _('Kilometers')),
(UNIT_METER, _('Meters')),
(UNIT_MILE, _('Miles')),
(UNIT_FOOT, _('Feet')),
)

View File

@ -105,7 +105,7 @@ class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = WirelessLink model = WirelessLink
fields = ('id', 'ssid', 'auth_psk', 'description') fields = ('id', 'ssid', 'auth_psk', 'distance', 'distance_unit', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -125,6 +125,17 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label=_('Pre-shared key') label=_('Pre-shared key')
) )
distance = forms.DecimalField(
label=_('Distance'),
min_value=0,
required=False
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
required=False,
initial=''
)
description = forms.CharField( description = forms.CharField(
label=_('Description'), label=_('Description'),
max_length=200, max_length=200,
@ -135,8 +146,9 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
model = WirelessLink model = WirelessLink
fieldsets = ( fieldsets = (
FieldSet('ssid', 'status', 'tenant', 'description'), FieldSet('ssid', 'status', 'tenant', 'description'),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')) FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
FieldSet('distance', 'distance_unit', name=_('Attributes')),
) )
nullable_fields = ( nullable_fields = (
'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments', 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'comments',
) )

View File

@ -112,10 +112,16 @@ class WirelessLinkImportForm(NetBoxModelImportForm):
required=False, required=False,
help_text=_('Authentication cipher') help_text=_('Authentication cipher')
) )
distance_unit = CSVChoiceField(
label=_('Distance unit'),
choices=WirelessLinkDistanceUnitChoices,
required=False,
help_text=_('Distance unit')
)
class Meta: class Meta:
model = WirelessLink model = WirelessLink
fields = ( fields = (
'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
'comments', 'tags', 'distance', 'distance_unit', 'description', 'comments', 'tags',
) )

View File

@ -71,7 +71,7 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = WirelessLink model = WirelessLink
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('ssid', 'status', name=_('Attributes')), FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
) )
@ -98,4 +98,13 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Pre-shared key'), label=_('Pre-shared key'),
required=False required=False
) )
distance = forms.DecimalField(
label=_('Distance'),
required=False,
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
required=False
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -159,7 +159,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
fieldsets = ( fieldsets = (
FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')), FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')),
FieldSet('site_b', 'location_b', 'device_b', 'interface_b', name=_('Side B')), FieldSet('site_b', 'location_b', 'device_b', 'interface_b', name=_('Side B')),
FieldSet('status', 'ssid', 'description', 'tags', name=_('Link')), FieldSet('status', 'ssid', 'distance', 'distance_unit', 'description', 'tags', name=_('Link')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
) )
@ -168,8 +168,8 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
model = WirelessLink model = WirelessLink
fields = [ fields = [
'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b', 'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b',
'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
'comments', 'tags', 'distance', 'distance_unit', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
'auth_psk': PasswordInput( 'auth_psk': PasswordInput(

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.6 on 2024-06-12 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireless', '0001_squashed_0008'),
]
operations = [
migrations.AddField(
model_name='wirelesslink',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
),
migrations.AddField(
model_name='wirelesslink',
name='distance',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
),
migrations.AddField(
model_name='wirelesslink',
name='distance_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices from dcim.choices import LinkStatusChoices
from dcim.constants import WIRELESS_IFACE_TYPES from dcim.constants import WIRELESS_IFACE_TYPES
from netbox.models import NestedGroupModel, PrimaryModel from netbox.models import NestedGroupModel, PrimaryModel
from utilities.conversion import to_meters
from .choices import * from .choices import *
from .constants import * from .constants import *
@ -160,6 +161,26 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
choices=LinkStatusChoices, choices=LinkStatusChoices,
default=LinkStatusChoices.STATUS_CONNECTED default=LinkStatusChoices.STATUS_CONNECTED
) )
distance = models.DecimalField(
verbose_name=_('distance'),
max_digits=8,
decimal_places=2,
blank=True,
null=True
)
distance_unit = models.CharField(
verbose_name=_('distance unit'),
max_length=50,
choices=WirelessLinkDistanceUnitChoices,
blank=True,
)
# Stores the normalized distance (in meters) for database ordering
_abs_distance = models.DecimalField(
max_digits=10,
decimal_places=4,
blank=True,
null=True
)
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -208,6 +229,11 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
return LinkStatusChoices.colors.get(self.status) return LinkStatusChoices.colors.get(self.status)
def clean(self): def clean(self):
super().clean()
# Validate distance and distance_unit
if self.distance is not None and not self.distance_unit:
raise ValidationError(_("Must specify a unit when setting a wireless distance"))
# Validate interface types # Validate interface types
if self.interface_a.type not in WIRELESS_IFACE_TYPES: if self.interface_a.type not in WIRELESS_IFACE_TYPES:
@ -224,6 +250,15 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Store the given distance (if any) in meters for use in database ordering
if self.distance is not None and self.distance_unit:
self._abs_distance = to_meters(self.distance, self.distance_unit)
else:
self._abs_distance = None
# Clear distance_unit if no distance is defined
if self.distance is None:
self.distance_unit = ''
# Store the parent Device for the A and B interfaces # Store the parent Device for the A and B interfaces
self._interface_a_device = self.interface_a.device self._interface_a_device = self.interface_a.device

View File

@ -0,0 +1,4 @@
WIRELESS_LINK_DISTANCE = """
{% load helpers %}
{% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
"""

View File

@ -4,6 +4,7 @@ import django_tables2 as tables
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin from tenancy.tables import TenancyColumnsMixin
from wireless.models import * from wireless.models import *
from .template_code import WIRELESS_LINK_DISTANCE
__all__ = ( __all__ = (
'WirelessLinkTable', 'WirelessLinkTable',
@ -36,6 +37,10 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Interface B'), verbose_name=_('Interface B'),
linkify=True linkify=True
) )
distance = columns.TemplateColumn(
template_code=WIRELESS_LINK_DISTANCE,
order_by=('_abs_distance')
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='wireless:wirelesslink_list' url_name='wireless:wirelesslink_list'
) )
@ -44,7 +49,8 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
model = WirelessLink model = WirelessLink
fields = ( fields = (
'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'tenant', 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'tenant',
'tenant_group', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', 'created', 'last_updated', 'tenant_group', 'distance', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags',
'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'auth_type', 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'auth_type',

View File

@ -113,6 +113,8 @@ class WirelessLinkTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'ssid', 'url'] brief_fields = ['description', 'display', 'id', 'ssid', 'url']
bulk_update_data = { bulk_update_data = {
'status': 'planned', 'status': 'planned',
'distance': 100,
'distance_unit': 'm',
} }
@classmethod @classmethod

View File

@ -260,6 +260,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
auth_psk='PSK1', auth_psk='PSK1',
tenant=tenants[0], tenant=tenants[0],
distance=10,
distance_unit=WirelessLinkDistanceUnitChoices.UNIT_FOOT,
description='foobar1' description='foobar1'
).save() ).save()
WirelessLink( WirelessLink(
@ -271,6 +273,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
auth_psk='PSK2', auth_psk='PSK2',
tenant=tenants[1], tenant=tenants[1],
distance=20,
distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
description='foobar2' description='foobar2'
).save() ).save()
WirelessLink( WirelessLink(
@ -281,6 +285,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
auth_psk='PSK3', auth_psk='PSK3',
distance=30,
distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
tenant=tenants[2], tenant=tenants[2],
).save() ).save()
WirelessLink( WirelessLink(
@ -313,6 +319,14 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'auth_psk': ['PSK1', 'PSK2']} params = {'auth_psk': ['PSK1', 'PSK2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_distance(self):
params = {'distance': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_distance_unit(self):
params = {'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self): def test_description(self):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -160,6 +160,8 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'interface_a': interfaces[6].pk, 'interface_a': interfaces[6].pk,
'interface_b': interfaces[7].pk, 'interface_b': interfaces[7].pk,
'status': LinkStatusChoices.STATUS_PLANNED, 'status': LinkStatusChoices.STATUS_PLANNED,
'distance': 100,
'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT,
'tenant': tenants[1].pk, 'tenant': tenants[1].pk,
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -180,4 +182,6 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.bulk_edit_data = { cls.bulk_edit_data = {
'status': LinkStatusChoices.STATUS_PLANNED, 'status': LinkStatusChoices.STATUS_PLANNED,
'distance': 50,
'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_METER,
} }