mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Closes #284: Added interface_ordering field to DeviceType
This commit is contained in:
parent
2ef1e623a3
commit
c9e7c12463
@ -138,7 +138,8 @@ class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields']
|
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||||
|
'comments', 'custom_fields']
|
||||||
|
|
||||||
def get_subdevice_role(self, obj):
|
def get_subdevice_role(self, obj):
|
||||||
return {
|
return {
|
||||||
@ -198,9 +199,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
|||||||
|
|
||||||
class Meta(DeviceTypeSerializer.Meta):
|
class Meta(DeviceTypeSerializer.Meta):
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||||
'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates',
|
'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
|
||||||
'interface_templates']
|
'power_outlet_templates', 'interface_templates']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -17,9 +17,9 @@ from formfields import MACAddressFormField
|
|||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||||
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -263,13 +263,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||||
'is_pdu', 'is_network_device', 'subdevice_role', 'comments']
|
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
|
||||||
|
labels = {
|
||||||
|
'interface_ordering': 'Order interfaces by',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
|
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-01-06 16:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0024_site_add_contact_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='interface_ordering',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1),
|
||||||
|
),
|
||||||
|
]
|
@ -56,6 +56,13 @@ SUBDEVICE_ROLE_CHOICES = (
|
|||||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IFACE_ORDERING_POSITION = 1
|
||||||
|
IFACE_ORDERING_NAME = 2
|
||||||
|
IFACE_ORDERING_CHOICES = [
|
||||||
|
[IFACE_ORDERING_POSITION, 'Slot/position'],
|
||||||
|
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
|
||||||
|
]
|
||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
# Ethernet
|
# Ethernet
|
||||||
@ -182,45 +189,6 @@ RPC_CLIENT_CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def order_interfaces(queryset):
|
|
||||||
"""
|
|
||||||
Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the
|
|
||||||
following pattern:
|
|
||||||
|
|
||||||
{a}/{b}/{c}:{d}
|
|
||||||
|
|
||||||
Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the
|
|
||||||
interface's type) is then used to order any duplicate slot/position tuples. If any fields are not contained by an
|
|
||||||
interface name, those fields are treated as null. Null values are ordered after all other values. For example:
|
|
||||||
|
|
||||||
et-0/0/0
|
|
||||||
et-0/0/1
|
|
||||||
et-0/1/0
|
|
||||||
xe-0/1/1:0
|
|
||||||
xe-0/1/1:1
|
|
||||||
xe-0/1/1:2
|
|
||||||
xe-0/1/1:3
|
|
||||||
et-0/1/2
|
|
||||||
...
|
|
||||||
et-0/1/9
|
|
||||||
et-0/1/10
|
|
||||||
et-0/1/11
|
|
||||||
et-1/0/0
|
|
||||||
et-1/0/1
|
|
||||||
...
|
|
||||||
vlan1
|
|
||||||
vlan10
|
|
||||||
"""
|
|
||||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
|
||||||
ordering = ('_id1', '_id2', '_id3', '_id4', 'name')
|
|
||||||
return queryset.extra(select={
|
|
||||||
'_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
|
||||||
'_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
|
||||||
'_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
|
||||||
'_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
|
||||||
}).order_by(*ordering)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@ -548,6 +516,8 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
||||||
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
||||||
help_text="Device consumes both front and rear rack faces")
|
help_text="Device consumes both front and rear rack faces")
|
||||||
|
interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES,
|
||||||
|
default=IFACE_ORDERING_POSITION)
|
||||||
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
||||||
help_text="This type of device has console server ports")
|
help_text="This type of device has console server ports")
|
||||||
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
||||||
@ -700,15 +670,40 @@ class PowerOutletTemplate(models.Model):
|
|||||||
|
|
||||||
class InterfaceManager(models.Manager):
|
class InterfaceManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||||
qs = super(InterfaceManager, self).get_queryset()
|
"""
|
||||||
return order_interfaces(qs)
|
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
|
||||||
|
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||||
|
|
||||||
def virtual(self):
|
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||||
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
slot, subslot, position, and channel:
|
||||||
|
|
||||||
def physical(self):
|
{name}{slot}/{subslot}/{position}:{channel}
|
||||||
return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL)
|
|
||||||
|
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||||
|
be parsed as follows:
|
||||||
|
|
||||||
|
name = 'GigabitEthernet'
|
||||||
|
slot = None
|
||||||
|
subslot = 0
|
||||||
|
position = 1
|
||||||
|
channel = None
|
||||||
|
|
||||||
|
The chosen sorting method will determine which fields are ordered first in the query.
|
||||||
|
"""
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||||
|
ordering = {
|
||||||
|
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'),
|
||||||
|
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'),
|
||||||
|
}[method]
|
||||||
|
return queryset.extra(select={
|
||||||
|
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||||
|
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
||||||
|
}).order_by(*ordering)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplate(models.Model):
|
class InterfaceTemplate(models.Model):
|
||||||
|
@ -232,6 +232,7 @@ class DeviceTypeTest(APITestCase):
|
|||||||
'part_number',
|
'part_number',
|
||||||
'u_height',
|
'u_height',
|
||||||
'is_full_depth',
|
'is_full_depth',
|
||||||
|
'interface_ordering',
|
||||||
'is_console_server',
|
'is_console_server',
|
||||||
'is_pdu',
|
'is_pdu',
|
||||||
'is_network_device',
|
'is_network_device',
|
||||||
|
@ -358,10 +358,14 @@ def devicetype(request, pk):
|
|||||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
)
|
)
|
||||||
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
mgmt_interface_table = tables.InterfaceTemplateTable(
|
||||||
mgmt_only=True))
|
InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
||||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
mgmt_only=True)
|
||||||
mgmt_only=False))
|
)
|
||||||
|
interface_table = tables.InterfaceTemplateTable(
|
||||||
|
InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
||||||
|
mgmt_only=False)
|
||||||
|
)
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
)
|
)
|
||||||
@ -597,16 +601,18 @@ def device(request, pk):
|
|||||||
power_outlets = natsorted(
|
power_outlets = natsorted(
|
||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related(
|
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||||
'connected_as_a__interface_b__device',
|
.filter(device=device, mgmt_only=False).select_related(
|
||||||
'connected_as_b__interface_a__device',
|
'connected_as_a__interface_b__device',
|
||||||
'circuit_termination__circuit',
|
'connected_as_b__interface_a__device',
|
||||||
)
|
'circuit_termination__circuit',
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related(
|
)
|
||||||
'connected_as_a__interface_b__device',
|
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||||
'connected_as_b__interface_a__device',
|
.filter(device=device, mgmt_only=True).select_related(
|
||||||
'circuit_termination__circuit',
|
'connected_as_a__interface_b__device',
|
||||||
)
|
'connected_as_b__interface_a__device',
|
||||||
|
'circuit_termination__circuit',
|
||||||
|
)
|
||||||
device_bays = natsorted(
|
device_bays = natsorted(
|
||||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
key=attrgetter('name')
|
key=attrgetter('name')
|
||||||
|
@ -72,6 +72,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Interface Ordering</td>
|
||||||
|
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||||
|
@ -11,6 +11,12 @@
|
|||||||
{% render_field form.part_number %}
|
{% render_field form.part_number %}
|
||||||
{% render_field form.u_height %}
|
{% render_field form.u_height %}
|
||||||
{% render_field form.is_full_depth %}
|
{% render_field form.is_full_depth %}
|
||||||
|
{% render_field form.interface_ordering %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Function</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.is_console_server %}
|
{% render_field form.is_console_server %}
|
||||||
{% render_field form.is_pdu %}
|
{% render_field form.is_pdu %}
|
||||||
{% render_field form.is_network_device %}
|
{% render_field form.is_network_device %}
|
||||||
|
Loading…
Reference in New Issue
Block a user