Browse Source

Closes #284: Added interface_ordering field to DeviceType

Jeremy Stretch 8 years ago
parent
commit
c9e7c12463

+ 5 - 4
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']
 
 
 #

+ 8 - 4
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 = []

+ 20 - 0
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),
+        ),
+    ]

+ 41 - 46
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).
+
+        To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
+        slot, subslot, position, and channel:
 
-    def virtual(self):
-        return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
+            {name}{slot}/{subslot}/{position}:{channel}
 
-    def physical(self):
-        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):

+ 1 - 0
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',

+ 20 - 14
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')

+ 4 - 0
netbox/templates/dcim/devicetype.html

@@ -73,6 +73,10 @@
                     </td>
                 </tr>
                 <tr>
+                    <td>Interface Ordering</td>
+                    <td>{{ devicetype.get_interface_ordering_display }}</td>
+                </tr>
+                <tr>
                     <td>Instances</td>
                     <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
                 </tr>

+ 6 - 0
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 %}
+        </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_pdu %}
             {% render_field form.is_network_device %}