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:
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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.
|
||||||
|
@ -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',
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
17
netbox/dcim/migrations/0134_interface_wwn.py
Normal file
17
netbox/dcim/migrations/0134_interface_wwn.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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,
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>]
|
||||||
|
Reference in New Issue
Block a user