diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 4758177e3..4856c1a4c 100644 --- a/docs/release-notes/version-3.1.md +++ b/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 diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8e2fa15af..a7d2e88da 100644 --- a/netbox/dcim/api/serializers.py +++ b/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', diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 21af2ed14..d3afe5c08 100644 --- a/netbox/dcim/fields.py +++ b/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. diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6f2c23c90..9457507bb 100644 --- a/netbox/dcim/filtersets.py +++ b/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', diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index c1b1bcb3a..5cfe86118 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/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): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 072cdf0e0..8f7755869 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/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): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 95ff9aa3d..0079217ab 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/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) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 009e1fe3f..0b6e66c3c 100644 --- a/netbox/dcim/forms/models.py +++ b/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 = { diff --git a/netbox/dcim/migrations/0134_interface_wwn.py b/netbox/dcim/migrations/0134_interface_wwn.py new file mode 100644 index 000000000..0739edbbb --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index a321c8059..386776b41 100644 --- a/netbox/dcim/models/device_components.py +++ b/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, diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index c22e673b7..c2b4b907b 100644 --- a/netbox/dcim/tables/devices.py +++ b/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', ) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 18eaeec3b..00904d444 100644 --- a/netbox/dcim/tests/test_views.py +++ b/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', diff --git a/netbox/netbox/graphql/__init__.py b/netbox/netbox/graphql/__init__.py index 069f6a9c8..0ad25a541 100644 --- a/netbox/netbox/graphql/__init__.py +++ b/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) diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 3a4d16db3..f9a9b0425 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -91,6 +91,10 @@