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

Closes #1337: Add WWN field to interfaces

This commit is contained in:
jeremystretch
2021-10-07 15:09:42 -04:00
parent 6463fd902c
commit 18c3bb673f
16 changed files with 107 additions and 19 deletions

View File

@ -3,6 +3,10 @@
!!! warning "PostgreSQL 10 Required" !!! warning "PostgreSQL 10 Required"
NetBox v3.1 requires PostgreSQL 10 or later. 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 ### Other Changes
* [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10 * [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10

View File

@ -645,7 +645,7 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', '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', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'_occupied', '_occupied',

View File

@ -2,11 +2,30 @@ from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models 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 ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from .lookups import PathContains 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): class ASNField(models.BigIntegerField):
description = "32-bit ASN field" description = "32-bit ASN field"
@ -24,10 +43,6 @@ class ASNField(models.BigIntegerField):
return super().formfield(**defaults) return super().formfield(**defaults)
class mac_unix_expanded_uppercase(mac_unix_expanded):
word_fmt = '%.2X'
class MACAddressField(models.Field): class MACAddressField(models.Field):
description = "PostgreSQL MAC Address field" description = "PostgreSQL MAC Address field"
@ -42,8 +57,8 @@ class MACAddressField(models.Field):
return value return value
try: try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase) return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError as e: except AddrFormatError:
raise ValidationError("Invalid MAC address format: {}".format(value)) raise ValidationError(f"Invalid MAC address format: {value}")
def db_type(self, connection): def db_type(self, connection):
return 'macaddr' return 'macaddr'
@ -54,6 +69,32 @@ class MACAddressField(models.Field):
return str(self.to_python(value)) 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): class PathField(ArrayField):
""" """
An ArrayField which holds a set of objects, each identified by a (type, ID) tuple. An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.

View File

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

View File

@ -921,7 +921,8 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm( class InterfaceBulkEditForm(
form_from_model(Interface, [ 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, BootstrapMixin,
AddRemoveTagsForm, AddRemoveTagsForm,
@ -972,7 +973,8 @@ class InterfaceBulkEditForm(
class Meta: class Meta:
nullable_fields = [ 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): def __init__(self, *args, **kwargs):

View File

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

View File

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

View File

@ -1091,7 +1091,7 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
class Meta: class Meta:
model = Interface model = Interface
fields = [ 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', 'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
] ]
widgets = { widgets = {

View File

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

View File

@ -9,7 +9,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import MACAddressField from dcim.fields import MACAddressField, WWNField
from dcim.svg import CableTraceSVG from dcim.svg import CableTraceSVG
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import PrimaryModel from netbox.models import PrimaryModel
@ -511,6 +511,12 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
verbose_name='Management only', verbose_name='Management only',
help_text='This interface is used only for out-of-band management' 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( untagged_vlan = models.ForeignKey(
to='ipam.VLAN', to='ipam.VLAN',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,

View File

@ -492,7 +492,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( 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', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans', 'untagged_vlan', 'tagged_vlans',
) )
@ -524,7 +524,7 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( 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', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans', 'actions', 'untagged_vlan', 'tagged_vlans', 'actions',
) )

View File

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

View File

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

View File

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

View File

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

View File

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