Просмотр исходного кода

Store channel frequency & width as independent values

jeremystretch 4 лет назад
Родитель
Сommit
075f4907ef

+ 4 - 4
netbox/dcim/api/serializers.py

@@ -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):

+ 2 - 2
netbox/dcim/forms/bulk_edit.py

@@ -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):

+ 2 - 1
netbox/dcim/forms/bulk_import.py

@@ -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):

+ 5 - 1
netbox/dcim/forms/filtersets.py

@@ -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)
 

+ 4 - 2
netbox/dcim/forms/models.py

@@ -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):

+ 8 - 4
netbox/dcim/forms/object_create.py

@@ -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):

+ 6 - 1
netbox/dcim/migrations/0138_wireless.py

@@ -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',

+ 35 - 5
netbox/dcim/models/device_components.py

@@ -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:
-            raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
+
+        # 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]:

+ 3 - 2
netbox/dcim/tables/devices.py

@@ -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')
 

+ 17 - 1
netbox/templates/dcim/interface.html

@@ -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>

+ 1 - 0
netbox/templates/dcim/interface_edit.html

@@ -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>

+ 20 - 0
netbox/templates/wireless/inc/wirelesslink_interface.html

@@ -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>

+ 14 - 0
netbox/utilities/templatetags/helpers.py

@@ -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):
     """

+ 67 - 69
netbox/wireless/choices.py

@@ -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)',
             (

+ 8 - 2
netbox/wireless/forms/models.py

@@ -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 - 0
netbox/wireless/utils.py

@@ -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]