From c9e7c12463a529177a73df3d545756663218ac93 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Jan 2017 12:59:49 -0500 Subject: [PATCH] Closes #284: Added interface_ordering field to DeviceType --- netbox/dcim/api/serializers.py | 9 +- netbox/dcim/forms.py | 12 ++- .../0025_devicetype_add_interface_ordering.py | 20 +++++ netbox/dcim/models.py | 87 +++++++++---------- netbox/dcim/tests/test_apis.py | 1 + netbox/dcim/views.py | 34 +++++--- netbox/templates/dcim/devicetype.html | 4 + netbox/templates/dcim/devicetype_edit.html | 6 ++ 8 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 3b1ab720c..698e7d2d6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -138,7 +138,8 @@ class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = DeviceType 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): return { @@ -198,9 +199,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer): class Meta(DeviceTypeSerializer.Meta): 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', - 'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates', - 'interface_templates'] + 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', + 'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates', + 'power_outlet_templates', 'interface_templates'] # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d82f28090..cb35ff881 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -17,9 +17,9 @@ from formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, - Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, - Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, - Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, + Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, + RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD ) @@ -263,13 +263,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): class Meta: model = DeviceType 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): pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), 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: nullable_fields = [] diff --git a/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py b/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py new file mode 100644 index 000000000..d1263cb89 --- /dev/null +++ b/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py @@ -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), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 790128340..030de3436 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -56,6 +56,13 @@ SUBDEVICE_ROLE_CHOICES = ( (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 IFACE_FF_VIRTUAL = 0 # 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 # @@ -548,6 +516,8 @@ class DeviceType(models.Model, CustomFieldModel): u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1) is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth", 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', help_text="This type of device has console server ports") is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU', @@ -700,15 +670,40 @@ class PowerOutletTemplate(models.Model): class InterfaceManager(models.Manager): - def get_queryset(self): - qs = super(InterfaceManager, self).get_queryset() - return order_interfaces(qs) + def order_naturally(self, method=IFACE_ORDERING_POSITION): + """ + 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): - return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) + To order interfaces naturally, the `name` field is split into five distinct components: leading text (name), + slot, subslot, position, and channel: - def physical(self): - return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL) + {name}{slot}/{subslot}/{position}:{channel} + + 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): diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 1305d7e37..3cef01701 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -232,6 +232,7 @@ class DeviceTypeTest(APITestCase): 'part_number', 'u_height', 'is_full_depth', + 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 58c38bfb0..3ac48a82f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -358,10 +358,14 @@ def devicetype(request, pk): poweroutlet_table = tables.PowerOutletTemplateTable( natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) ) - mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype, - mgmt_only=True)) - interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype, - mgmt_only=False)) + mgmt_interface_table = tables.InterfaceTemplateTable( + InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=True) + ) + interface_table = tables.InterfaceTemplateTable( + InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=False) + ) devicebay_table = tables.DeviceBayTemplateTable( natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) ) @@ -597,16 +601,18 @@ def device(request, pk): power_outlets = natsorted( PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') ) - interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related( - 'connected_as_a__interface_b__device', - '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', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=False).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) + mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=True).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 9bc16d146..a9a9fa130 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -72,6 +72,10 @@ {% endif %} + + Interface Ordering + {{ devicetype.get_interface_ordering_display }} + Instances {{ devicetype.instances.count }} diff --git a/netbox/templates/dcim/devicetype_edit.html b/netbox/templates/dcim/devicetype_edit.html index 929da06b8..d2a107607 100644 --- a/netbox/templates/dcim/devicetype_edit.html +++ b/netbox/templates/dcim/devicetype_edit.html @@ -11,6 +11,12 @@ {% render_field form.part_number %} {% render_field form.u_height %} {% render_field form.is_full_depth %} + {% render_field form.interface_ordering %} + + +
+
Function
+
{% render_field form.is_console_server %} {% render_field form.is_pdu %} {% render_field form.is_network_device %}