1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Store channel frequency & width as independent values

This commit is contained in:
jeremystretch
2021-10-15 11:35:10 -04:00
parent b7317bfe29
commit 075f4907ef
16 changed files with 223 additions and 94 deletions

View File

@ -651,10 +651,10 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
model = Interface
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'untagged_vlan',
'tagged_vlans', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', '_occupied',
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer',
'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
]
def validate(self, data):

View File

@ -936,7 +936,7 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm(
form_from_model(Interface, [
'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
'mode', 'rf_role', 'rf_channel', 'rf_channel_width',
'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
]),
BootstrapMixin,
AddRemoveTagsForm,
@ -988,7 +988,7 @@ class InterfaceBulkEditForm(
class Meta:
nullable_fields = [
'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
'rf_channel_width', 'untagged_vlan', 'tagged_vlans',
'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans',
]
def __init__(self, *args, **kwargs):

View File

@ -595,7 +595,8 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
model = Interface
fields = (
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn',
'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width',
'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width',
)
def __init__(self, *args, **kwargs):

View File

@ -1014,9 +1014,13 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
widget=StaticSelectMultiple(),
label='Wireless channel'
)
rf_channel_frequency = forms.IntegerField(
required=False,
label='Channel frequency (MHz)'
)
rf_channel_width = forms.IntegerField(
required=False,
label='Channel width (kHz)'
label='Channel width (MHz)'
)
tag = TagFilterField(model)

View File

@ -1108,8 +1108,8 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
model = Interface
fields = [
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'wireless_lans',
'untagged_vlan', 'tagged_vlans', 'tags',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
@ -1123,6 +1123,8 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
}
help_texts = {
'mode': INTERFACE_MODE_HELP_TEXT,
'rf_channel_frequency': "Populated by selected channel (if set)",
'rf_channel_width': "Populated by selected channel (if set)",
}
def __init__(self, *args, **kwargs):

View File

@ -480,9 +480,13 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
widget=StaticSelect(),
label='Wireless channel'
)
rf_channel_width = forms.IntegerField(
rf_channel_frequency = forms.DecimalField(
required=False,
label='Channel width'
label='Channel frequency (MHz)'
)
rf_channel_width = forms.DecimalField(
required=False,
label='Channel width (MHz)'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
@ -494,8 +498,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_width', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags'
'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
)
def __init__(self, *args, **kwargs):

View File

@ -20,10 +20,15 @@ class Migration(migrations.Migration):
name='rf_channel',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='interface',
name='rf_channel_frequency',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
),
migrations.AddField(
model_name='interface',
name='rf_channel_width',
field=models.PositiveSmallIntegerField(blank=True, null=True),
field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True),
),
migrations.AddField(
model_name='interface',

View File

@ -19,6 +19,7 @@ from utilities.ordering import naturalize_interface
from utilities.querysets import RestrictedQuerySet
from utilities.query_functions import CollateAsChar
from wireless.choices import *
from wireless.utils import get_channel_attr
__all__ = (
@ -537,10 +538,19 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
blank=True,
verbose_name='Wireless channel'
)
rf_channel_width = models.PositiveSmallIntegerField(
rf_channel_frequency = models.DecimalField(
max_digits=7,
decimal_places=2,
blank=True,
null=True,
verbose_name='Channel width (kHz)'
verbose_name='Channel frequency (MHz)'
)
rf_channel_width = models.DecimalField(
max_digits=7,
decimal_places=3,
blank=True,
null=True,
verbose_name='Channel width (MHz)'
)
wireless_link = models.ForeignKey(
to='wireless.WirelessLink',
@ -641,13 +651,33 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
# RF channel attributes may be set only for wireless interfaces
# RF role & channel may only be set for wireless interfaces
if self.rf_role and not self.is_wireless:
raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."})
if self.rf_channel and not self.is_wireless:
raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."})
if self.rf_channel_width and not self.is_wireless:
# Validate channel frequency against interface type and selected channel (if any)
if self.rf_channel_frequency:
if not self.is_wireless:
raise ValidationError({
'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.",
})
if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
raise ValidationError({
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
})
elif self.rf_channel:
self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')
# Validate channel width against interface type and selected channel (if any)
if self.rf_channel_width:
if not self.is_wireless:
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
elif self.rf_channel:
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:

View File

@ -496,8 +496,9 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
model = Interface
fields = (
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color',
'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mark_connected',
'cable', 'cable_color', 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
'tagged_vlans',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')

View File

@ -276,9 +276,25 @@
<th scope="row">Channel</th>
<td>{{ object.get_rf_channel_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Channel Frequency</th>
<td>
{% if object.rf_channel_frequency %}
{{ object.rf_channel_frequency|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Channel Width</th>
<td>{{ object.rf_channel_width|placeholder }}</td>
<td>
{% if object.rf_channel_width %}
{{ object.rf_channel_width|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
</table>
</div>

View File

@ -36,6 +36,7 @@
</div>
{% render_field form.rf_role %}
{% render_field form.rf_channel %}
{% render_field form.rf_channel_frequency %}
{% render_field form.rf_channel_width %}
{% render_field form.wireless_lans %}
</div>

View File

@ -31,4 +31,24 @@
{{ interface.get_rf_channel_display|placeholder }}
</td>
</tr>
<tr>
<th scope="row">Channel Frequency</th>
<td>
{% if interface.rf_channel_frequency %}
{{ interface.rf_channel_frequency|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Channel Width</th>
<td>
{% if interface.rf_channel_width %}
{{ interface.rf_channel_width|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
</table>

View File

@ -1,4 +1,5 @@
import datetime
import decimal
import json
import re
from typing import Dict, Any
@ -146,6 +147,19 @@ def humanize_megabytes(mb):
return f'{mb} MB'
@register.filter()
def simplify_decimal(value):
"""
Return the simplest expression of a decimal value. Examples:
1.00 => '1'
1.20 => '1.2'
1.23 => '1.23'
"""
if type(value) is not decimal.Decimal:
return value
return str(value).rstrip('0.')
@register.filter()
def tzoffset(value):
"""

View File

@ -12,81 +12,79 @@ class WirelessRoleChoices(ChoiceSet):
class WirelessChannelChoices(ChoiceSet):
CHANNEL_AUTO = 'auto'
# 2.4 GHz
CHANNEL_24G_1 = '2.4g-1'
CHANNEL_24G_2 = '2.4g-2'
CHANNEL_24G_3 = '2.4g-3'
CHANNEL_24G_4 = '2.4g-4'
CHANNEL_24G_5 = '2.4g-5'
CHANNEL_24G_6 = '2.4g-6'
CHANNEL_24G_7 = '2.4g-7'
CHANNEL_24G_8 = '2.4g-8'
CHANNEL_24G_9 = '2.4g-9'
CHANNEL_24G_10 = '2.4g-10'
CHANNEL_24G_11 = '2.4g-11'
CHANNEL_24G_12 = '2.4g-12'
CHANNEL_24G_13 = '2.4g-13'
CHANNEL_24G_1 = '2.4g-1-2412-22'
CHANNEL_24G_2 = '2.4g-2-2417-22'
CHANNEL_24G_3 = '2.4g-3-2422-22'
CHANNEL_24G_4 = '2.4g-4-2427-22'
CHANNEL_24G_5 = '2.4g-5-2432-22'
CHANNEL_24G_6 = '2.4g-6-2437-22'
CHANNEL_24G_7 = '2.4g-7-2442-22'
CHANNEL_24G_8 = '2.4g-8-2447-22'
CHANNEL_24G_9 = '2.4g-9-2452-22'
CHANNEL_24G_10 = '2.4g-10-2457-22'
CHANNEL_24G_11 = '2.4g-11-2462-22'
CHANNEL_24G_12 = '2.4g-12-2467-22'
CHANNEL_24G_13 = '2.4g-13-2472-22'
# 5 GHz
CHANNEL_5G_32 = '5g-32'
CHANNEL_5G_34 = '5g-34'
CHANNEL_5G_36 = '5g-36'
CHANNEL_5G_38 = '5g-38'
CHANNEL_5G_40 = '5g-40'
CHANNEL_5G_42 = '5g-42'
CHANNEL_5G_44 = '5g-44'
CHANNEL_5G_46 = '5g-46'
CHANNEL_5G_48 = '5g-48'
CHANNEL_5G_50 = '5g-50'
CHANNEL_5G_52 = '5g-52'
CHANNEL_5G_54 = '5g-54'
CHANNEL_5G_56 = '5g-56'
CHANNEL_5G_58 = '5g-58'
CHANNEL_5G_60 = '5g-60'
CHANNEL_5G_62 = '5g-62'
CHANNEL_5G_64 = '5g-64'
CHANNEL_5G_100 = '5g-100'
CHANNEL_5G_102 = '5g-102'
CHANNEL_5G_104 = '5g-104'
CHANNEL_5G_106 = '5g-106'
CHANNEL_5G_108 = '5g-108'
CHANNEL_5G_110 = '5g-110'
CHANNEL_5G_112 = '5g-112'
CHANNEL_5G_114 = '5g-114'
CHANNEL_5G_116 = '5g-116'
CHANNEL_5G_118 = '5g-118'
CHANNEL_5G_120 = '5g-120'
CHANNEL_5G_122 = '5g-122'
CHANNEL_5G_124 = '5g-124'
CHANNEL_5G_126 = '5g-126'
CHANNEL_5G_128 = '5g-128'
CHANNEL_5G_132 = '5g-132'
CHANNEL_5G_134 = '5g-134'
CHANNEL_5G_136 = '5g-136'
CHANNEL_5G_138 = '5g-138'
CHANNEL_5G_140 = '5g-140'
CHANNEL_5G_142 = '5g-142'
CHANNEL_5G_144 = '5g-144'
CHANNEL_5G_149 = '5g-149'
CHANNEL_5G_151 = '5g-151'
CHANNEL_5G_153 = '5g-153'
CHANNEL_5G_155 = '5g-155'
CHANNEL_5G_157 = '5g-157'
CHANNEL_5G_159 = '5g-159'
CHANNEL_5G_161 = '5g-161'
CHANNEL_5G_163 = '5g-163'
CHANNEL_5G_165 = '5g-165'
CHANNEL_5G_167 = '5g-167'
CHANNEL_5G_169 = '5g-169'
CHANNEL_5G_171 = '5g-171'
CHANNEL_5G_173 = '5g-173'
CHANNEL_5G_175 = '5g-175'
CHANNEL_5G_177 = '5g-177'
CHANNEL_5G_32 = '5g-32-5160-20'
CHANNEL_5G_34 = '5g-34-5170-40'
CHANNEL_5G_36 = '5g-36-5180-20'
CHANNEL_5G_38 = '5g-38-5190-40'
CHANNEL_5G_40 = '5g-40-5200-20'
CHANNEL_5G_42 = '5g-42-5210-80'
CHANNEL_5G_44 = '5g-44-5220-20'
CHANNEL_5G_46 = '5g-46-5230-40'
CHANNEL_5G_48 = '5g-48-5240-20'
CHANNEL_5G_50 = '5g-50-5250-160'
CHANNEL_5G_52 = '5g-52-5260-20'
CHANNEL_5G_54 = '5g-54-5270-40'
CHANNEL_5G_56 = '5g-56-5280-20'
CHANNEL_5G_58 = '5g-58-5290-80'
CHANNEL_5G_60 = '5g-60-5300-20'
CHANNEL_5G_62 = '5g-62-5310-40'
CHANNEL_5G_64 = '5g-64-5320-20'
CHANNEL_5G_100 = '5g-100-5500-20'
CHANNEL_5G_102 = '5g-102-5510-40'
CHANNEL_5G_104 = '5g-104-5520-20'
CHANNEL_5G_106 = '5g-106-5530-80'
CHANNEL_5G_108 = '5g-108-5540-20'
CHANNEL_5G_110 = '5g-110-5550-40'
CHANNEL_5G_112 = '5g-112-5560-20'
CHANNEL_5G_114 = '5g-114-5570-160'
CHANNEL_5G_116 = '5g-116-5580-20'
CHANNEL_5G_118 = '5g-118-5590-40'
CHANNEL_5G_120 = '5g-120-5600-20'
CHANNEL_5G_122 = '5g-122-5610-80'
CHANNEL_5G_124 = '5g-124-5620-20'
CHANNEL_5G_126 = '5g-126-5630-40'
CHANNEL_5G_128 = '5g-128-5640-20'
CHANNEL_5G_132 = '5g-132-5660-20'
CHANNEL_5G_134 = '5g-134-5670-40'
CHANNEL_5G_136 = '5g-136-5680-20'
CHANNEL_5G_138 = '5g-138-5690-80'
CHANNEL_5G_140 = '5g-140-5700-20'
CHANNEL_5G_142 = '5g-142-5710-40'
CHANNEL_5G_144 = '5g-144-5720-20'
CHANNEL_5G_149 = '5g-149-5745-20'
CHANNEL_5G_151 = '5g-151-5755-40'
CHANNEL_5G_153 = '5g-153-5765-20'
CHANNEL_5G_155 = '5g-155-5775-80'
CHANNEL_5G_157 = '5g-157-5785-20'
CHANNEL_5G_159 = '5g-159-5795-40'
CHANNEL_5G_161 = '5g-161-5805-20'
CHANNEL_5G_163 = '5g-163-5815-160'
CHANNEL_5G_165 = '5g-165-5825-20'
CHANNEL_5G_167 = '5g-167-5835-40'
CHANNEL_5G_169 = '5g-169-5845-20'
CHANNEL_5G_171 = '5g-171-5855-80'
CHANNEL_5G_173 = '5g-173-5865-20'
CHANNEL_5G_175 = '5g-175-5875-40'
CHANNEL_5G_177 = '5g-177-5885-20'
CHOICES = (
(CHANNEL_AUTO, 'Auto'),
(
'2.4 GHz (802.11b/g/n/ax)',
(

View File

@ -56,7 +56,10 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
device_a = DynamicModelChoiceField(
queryset=Device.objects.all(),
label='Device A'
label='Device A',
initial_params={
'interfaces': '$interface_a'
}
)
interface_a = DynamicModelChoiceField(
queryset=Interface.objects.all(),
@ -69,7 +72,10 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm):
)
device_b = DynamicModelChoiceField(
queryset=Device.objects.all(),
label='Device B'
label='Device B',
initial_params={
'interfaces': '$interface_b'
}
)
interface_b = DynamicModelChoiceField(
queryset=Interface.objects.all(),

27
netbox/wireless/utils.py Normal file
View File

@ -0,0 +1,27 @@
from decimal import Decimal
from .choices import WirelessChannelChoices
__all__ = (
'get_channel_attr',
)
def get_channel_attr(channel, attr):
"""
Return the specified attribute of a given WirelessChannelChoices value.
"""
if channel not in WirelessChannelChoices.values():
raise ValueError(f"Invalid channel value: {channel}")
channel_values = channel.split('-')
attrs = {
'band': channel_values[0],
'id': int(channel_values[1]),
'frequency': Decimal(channel_values[2]),
'width': Decimal(channel_values[3]),
}
if attr not in attrs:
raise ValueError(f"Invalid channel attribute: {attr}")
return attrs[attr]