Convert interface models to use NaturalOrderingField

This commit is contained in:
Jeremy Stretch 2020-02-07 15:47:53 -05:00
parent 9adeed55fb
commit 7c74d2ca65
5 changed files with 120 additions and 65 deletions

View File

@ -1,18 +1,7 @@
from django.db.models import Manager, QuerySet from django.db.models import Manager, QuerySet
from django.db.models.expressions import RawSQL
from .constants import NONCONNECTABLE_IFACE_TYPES from .constants import NONCONNECTABLE_IFACE_TYPES
# Regular expressions for parsing Interface names
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class InterfaceQuerySet(QuerySet): class InterfaceQuerySet(QuerySet):
@ -27,47 +16,4 @@ class InterfaceQuerySet(QuerySet):
class InterfaceManager(Manager): class InterfaceManager(Manager):
def get_queryset(self): def get_queryset(self):
""" return InterfaceQuerySet(self.model, using=self._db)
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
and virtual circuit:
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are coalesced to zero or null. For example, an interface named
GigabitEthernet1/2/3 would be parsed as follows:
type = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = None
id = None
channel = 0
vc = 0
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
match any of the prescribed fields.
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
components.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = [
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
]
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)

View File

@ -0,0 +1,53 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def _update_model_names(model):
# Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name))
def naturalize_interfacetemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'InterfaceTemplate'))
def naturalize_interfaces(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'Interface'))
class Migration(migrations.Migration):
dependencies = [
('dcim', '0095_primary_model_ordering'),
]
operations = [
migrations.AlterModelOptions(
name='interface',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='interfacetemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AddField(
model_name='interface',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
),
migrations.AddField(
model_name='interfacetemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
),
migrations.RunPython(
code=naturalize_interfacetemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_interfaces,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -4,9 +4,9 @@ from django.db import models
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.managers import InterfaceManager
from extras.models import ObjectChange from extras.models import ObjectChange
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
@ -249,6 +249,12 @@ class InterfaceTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=InterfaceTypeChoices choices=InterfaceTypeChoices
@ -258,11 +264,9 @@ class InterfaceTemplate(ComponentTemplateModel):
verbose_name='Management only' verbose_name='Management only'
) )
objects = InterfaceManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -10,9 +10,9 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.exceptions import LoopDetected from dcim.exceptions import LoopDetected
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from dcim.managers import InterfaceManager
from extras.models import ObjectChange, TaggedItem from extras.models import ObjectChange, TaggedItem
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object from utilities.utils import serialize_object
from virtualization.choices import VMInterfaceTypeChoices from virtualization.choices import VMInterfaceTypeChoices
@ -529,6 +529,12 @@ class Interface(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
_connected_interface = models.OneToOneField( _connected_interface = models.OneToOneField(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -597,8 +603,6 @@ class Interface(CableTermination, ComponentModel):
blank=True, blank=True,
verbose_name='Tagged VLANs' verbose_name='Tagged VLANs'
) )
objects = InterfaceManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
@ -607,8 +611,9 @@ class Interface(CableTermination, ComponentModel):
] ]
class Meta: class Meta:
ordering = ['device', 'name'] # TODO: ordering and unique_together should include virtual_machine
unique_together = ['device', 'name'] ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -1,5 +1,14 @@
import re import re
INTERFACE_NAME_REGEX = r'(^(?P<type>[^\d\.:]+)?)' \
r'((?P<slot>\d+)/)?' \
r'((?P<subslot>\d+)/)?' \
r'((?P<position>\d+)/)?' \
r'((?P<subposition>\d+)/)?' \
r'((?P<id>\d+))?' \
r'(:(?P<channel>\d+))?' \
r'(.(?P<vc>\d+)$)?'
def naturalize(value, max_length=None, integer_places=8): def naturalize(value, max_length=None, integer_places=8):
""" """
@ -31,3 +40,41 @@ def naturalize(value, max_length=None, integer_places=8):
ret = ''.join(output) ret = ''.join(output)
return ret[:max_length] if max_length else ret return ret[:max_length] if max_length else ret
def naturalize_interface(value, max_length=None):
"""
Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
InterfaceManager.
:param value: The value to be naturalized
:param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.
"""
output = []
match = re.search(INTERFACE_NAME_REGEX, value)
if match is None:
return value
# First, we order by slot/position, padding each to four digits. If a field is not present,
# set it to 9999 to ensure it is ordered last.
for part_name in ('slot', 'subslot', 'position', 'subposition'):
part = match.group(part_name)
if part is not None:
output.append(part.rjust(4, '0'))
else:
output.append('9999')
# Append the type, if any.
if match.group('type') is not None:
output.append(match.group('type'))
# Finally, append any remaining fields, left-padding to eight digits each.
for part_name in ('id', 'channel', 'vc'):
part = match.group(part_name)
if part is not None:
output.append(part.rjust(6, '0'))
else:
output.append('000000')
ret = ''.join(output)
return ret[:max_length] if max_length else ret