2
0
Эх сурвалжийг харах

Closes #1337: Add WWN field to interfaces

jeremystretch 4 жил өмнө
parent
commit
18c3bb673f

+ 4 - 0
docs/release-notes/version-3.1.md

@@ -3,6 +3,10 @@
 !!! warning "PostgreSQL 10 Required"
     NetBox v3.1 requires PostgreSQL 10 or later.
 
+### Enhancements
+
+* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
+
 ### Other Changes
 
 * [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10

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

@@ -645,7 +645,7 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
         model = Interface
         fields = [
             'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
-            'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
+            'wwn', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
             'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
             'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
             '_occupied',

+ 48 - 7
netbox/dcim/fields.py

@@ -2,11 +2,30 @@ from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator, MaxValueValidator
 from django.db import models
-from netaddr import AddrFormatError, EUI, mac_unix_expanded
+from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
 
 from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
 from .lookups import PathContains
 
+__all__ = (
+    'ASNField',
+    'MACAddressField',
+    'PathField',
+    'WWNField',
+)
+
+
+class mac_unix_expanded_uppercase(mac_unix_expanded):
+    word_fmt = '%.2X'
+
+
+class eui64_unix_expanded_uppercase(eui64_unix_expanded):
+    word_fmt = '%.2X'
+
+
+#
+# Fields
+#
 
 class ASNField(models.BigIntegerField):
     description = "32-bit ASN field"
@@ -24,10 +43,6 @@ class ASNField(models.BigIntegerField):
         return super().formfield(**defaults)
 
 
-class mac_unix_expanded_uppercase(mac_unix_expanded):
-    word_fmt = '%.2X'
-
-
 class MACAddressField(models.Field):
     description = "PostgreSQL MAC Address field"
 
@@ -42,8 +57,8 @@ class MACAddressField(models.Field):
             return value
         try:
             return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
-        except AddrFormatError as e:
-            raise ValidationError("Invalid MAC address format: {}".format(value))
+        except AddrFormatError:
+            raise ValidationError(f"Invalid MAC address format: {value}")
 
     def db_type(self, connection):
         return 'macaddr'
@@ -54,6 +69,32 @@ class MACAddressField(models.Field):
         return str(self.to_python(value))
 
 
+class WWNField(models.Field):
+    description = "World Wide Name field"
+
+    def python_type(self):
+        return EUI
+
+    def from_db_value(self, value, expression, connection):
+        return self.to_python(value)
+
+    def to_python(self, value):
+        if value is None:
+            return value
+        try:
+            return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
+        except AddrFormatError:
+            raise ValidationError(f"Invalid WWN format: {value}")
+
+    def db_type(self, connection):
+        return 'macaddr8'
+
+    def get_prep_value(self, value):
+        if not value:
+            return None
+        return str(self.to_python(value))
+
+
 class PathField(ArrayField):
     """
     An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.

+ 2 - 1
netbox/dcim/filtersets.py

@@ -10,7 +10,7 @@ from tenancy.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from utilities.choices import ColorChoices
 from utilities.filters import (
-    ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
+    ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
     TreeNodeMultipleChoiceFilter,
 )
 from virtualization.models import Cluster
@@ -964,6 +964,7 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
         label='LAG interface (ID)',
     )
     mac_address = MultiValueMACAddressFilter()
+    wwn = MultiValueWWNFilter()
     tag = TagFilter()
     vlan_id = django_filters.CharFilter(
         method='filter_vlan_id',

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

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

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

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

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

@@ -957,7 +957,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     field_groups = [
         ['q', 'tag'],
-        ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'],
+        ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
     ]
     type = forms.MultipleChoiceField(
@@ -981,6 +981,10 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
         required=False,
         label='MAC address'
     )
+    wwn = forms.CharField(
+        required=False,
+        label='WWN'
+    )
     tag = TagFilterField(model)
 
 

+ 1 - 1
netbox/dcim/forms/models.py

@@ -1091,7 +1091,7 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
     class Meta:
         model = Interface
         fields = [
-            'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only',
+            'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
             'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
         ]
         widgets = {

+ 17 - 0
netbox/dcim/migrations/0134_interface_wwn.py

@@ -0,0 +1,17 @@
+import dcim.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0133_port_colors'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='interface',
+            name='wwn',
+            field=dcim.fields.WWNField(blank=True, null=True),
+        ),
+    ]

+ 7 - 1
netbox/dcim/models/device_components.py

@@ -9,7 +9,7 @@ from mptt.models import MPTTModel, TreeForeignKey
 
 from dcim.choices import *
 from dcim.constants import *
-from dcim.fields import MACAddressField
+from dcim.fields import MACAddressField, WWNField
 from dcim.svg import CableTraceSVG
 from extras.utils import extras_features
 from netbox.models import PrimaryModel
@@ -511,6 +511,12 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
         verbose_name='Management only',
         help_text='This interface is used only for out-of-band management'
     )
+    wwn = WWNField(
+        null=True,
+        blank=True,
+        verbose_name='WWN',
+        help_text='64-bit World Wide Name'
+    )
     untagged_vlan = models.ForeignKey(
         to='ipam.VLAN',
         on_delete=models.SET_NULL,

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

@@ -492,7 +492,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
     class Meta(DeviceComponentTable.Meta):
         model = Interface
         fields = (
-            'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
+            'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
             'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
             'untagged_vlan', 'tagged_vlans',
         )
@@ -524,7 +524,7 @@ class DeviceInterfaceTable(InterfaceTable):
     class Meta(DeviceComponentTable.Meta):
         model = Interface
         fields = (
-            'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
+            'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
             'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
             'untagged_vlan', 'tagged_vlans', 'actions',
         )

+ 3 - 0
netbox/dcim/tests/test_views.py

@@ -1469,6 +1469,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'enabled': False,
             'lag': interfaces[3].pk,
             'mac_address': EUI('01:02:03:04:05:06'),
+            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
             'mtu': 65000,
             'mgmt_only': True,
             'description': 'A front port',
@@ -1485,6 +1486,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'enabled': False,
             'lag': interfaces[3].pk,
             'mac_address': EUI('01:02:03:04:05:06'),
+            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
             'mtu': 2000,
             'mgmt_only': True,
             'description': 'A front port',
@@ -1499,6 +1501,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'enabled': True,
             'lag': interfaces[3].pk,
             'mac_address': EUI('01:02:03:04:05:06'),
+            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
             'mtu': 2000,
             'mgmt_only': True,
             'description': 'New description',

+ 2 - 1
netbox/netbox/graphql/__init__.py

@@ -2,7 +2,7 @@ import graphene
 from graphene_django.converter import convert_django_field
 from taggit.managers import TaggableManager
 
-from dcim.fields import MACAddressField
+from dcim.fields import MACAddressField, WWNField
 from ipam.fields import IPAddressField, IPNetworkField
 
 
@@ -17,6 +17,7 @@ def convert_field_to_tags_list(field, registry=None):
 @convert_django_field.register(IPAddressField)
 @convert_django_field.register(IPNetworkField)
 @convert_django_field.register(MACAddressField)
+@convert_django_field.register(WWNField)
 def convert_field_to_string(field, registry=None):
     # TODO: Update to use get_django_field_description under django_graphene v3.0
     return graphene.String(description=field.help_text, required=not field.null)

+ 4 - 0
netbox/templates/dcim/interface.html

@@ -91,6 +91,10 @@
                             <th scope="row">MAC Address</th>
                             <td><span class="text-monospace">{{ object.mac_address|placeholder }}</span></td>
                         </tr>
+                        <tr>
+                            <th scope="row">WWN</th>
+                            <td><span class="text-monospace">{{ object.wwn|placeholder }}</span></td>
+                        </tr>
                         <tr>
                             <th scope="row">802.1Q Mode</th>
                             <td>{{ object.get_mode_display|placeholder }}</td>

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

@@ -20,6 +20,7 @@
         {% render_field form.parent %}
         {% render_field form.lag %}
         {% render_field form.mac_address %}
+        {% render_field form.wwn %}
         {% render_field form.mtu %}
         {% render_field form.description %}
         {% render_field form.tags %}

+ 4 - 0
netbox/utilities/filters.py

@@ -57,6 +57,10 @@ class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
     field_class = multivalue_field_factory(MACAddressField)
 
 
+class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(MACAddressField)
+
+
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]