Extend Cable model to support multiple A/B terminations

This commit is contained in:
jeremystretch 2022-04-25 17:10:15 -04:00
parent 6c290353c1
commit 4bb9b6ee26
16 changed files with 368 additions and 283 deletions

View File

@ -977,8 +977,8 @@ class CableSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'id', 'url', 'display', 'termination_a_type', 'termination_a_ids', 'termination_a', 'termination_b_type',
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'termination_b_ids', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
'tags', 'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@ -986,14 +986,12 @@ class CableSerializer(NetBoxModelSerializer):
""" """
Serialize a nested representation of a termination. Serialize a nested representation of a termination.
""" """
if side.lower() not in ['a', 'b']: assert side.lower() in ('a', 'b')
raise ValueError("Termination side must be either A or B.") termination_type = getattr(obj, f'termination_{side}_type').model_class()
termination = getattr(obj, 'termination_{}'.format(side.lower())) termination = getattr(obj, f'termination_{side}')
if termination is None: serializer = get_serializer_for_model(termination_type, prefix='Nested')
return None
serializer = get_serializer_for_model(termination, prefix='Nested')
context = {'request': self.context['request']} context = {'request': self.context['request']}
data = serializer(termination, context=context).data data = serializer(termination, context=context, many=True).data
return data return data

View File

@ -647,9 +647,7 @@ class InventoryItemRoleViewSet(NetBoxModelViewSet):
class CableViewSet(NetBoxModelViewSet): class CableViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = Cable.objects.prefetch_related( queryset = Cable.objects.all()
'termination_a', 'termination_b'
)
serializer_class = serializers.CableSerializer serializer_class = serializers.CableSerializer
filterset_class = filtersets.CableFilterSet filterset_class = filtersets.CableFilterSet

View File

@ -1499,9 +1499,9 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
termination_a_type = ContentTypeFilter() termination_a_type = ContentTypeFilter()
termination_a_id = MultiValueNumberFilter() termination_a_ids = MultiValueNumberFilter()
termination_b_type = ContentTypeFilter() termination_b_type = ContentTypeFilter()
termination_b_id = MultiValueNumberFilter() termination_b_ids = MultiValueNumberFilter()
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CableTypeChoices choices=CableTypeChoices
) )
@ -1537,7 +1537,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = Cable model = Cable
fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id'] fields = ['id', 'label', 'length', 'length_unit', 'termination_a_ids', 'termination_b_ids']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1546,8 +1546,8 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
queryset = queryset.filter( queryset = queryset.filter(
Q(**{'_termination_a_{}__in'.format(name): value}) | Q(**{f'_termination_a_{name}__in': value}) |
Q(**{'_termination_b_{}__in'.format(name): value}) Q(**{f'_termination_b_{name}__in': value})
) )
return queryset return queryset

View File

@ -2,7 +2,7 @@ from circuits.models import Circuit, CircuitTermination, Provider
from dcim.models import * from dcim.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms import DynamicModelChoiceField, StaticSelect from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
__all__ = ( __all__ = (
'ConnectCableToCircuitTerminationForm', 'ConnectCableToCircuitTerminationForm',
@ -22,7 +22,7 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
Base form for connecting a Cable to a Device component Base form for connecting a Cable to a Device component
""" """
# Termination A # Termination A
termination_a_id = DynamicModelChoiceField( termination_a_ids = DynamicModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied' disabled_indicator='_occupied'
@ -87,8 +87,8 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'termination_a_id', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_a_ids', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status', 'tenant_group', 'termination_b_rack', 'termination_b_device', 'termination_b_ids', 'type', 'status', 'tenant_group',
'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
] ]
widgets = { widgets = {
@ -97,17 +97,17 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
'length_unit': StaticSelect, 'length_unit': StaticSelect,
} }
def clean_termination_a_id(self): def clean_termination_a_ids(self):
# Return the PK rather than the object # Return the PK rather than the object
return getattr(self.cleaned_data['termination_a_id'], 'pk', None) return [getattr(obj, 'pk') for obj in self.cleaned_data['termination_a_ids']]
def clean_termination_b_id(self): def clean_termination_b_ids(self):
# Return the PK rather than the object # Return the PK rather than the object
return getattr(self.cleaned_data['termination_b_id'], 'pk', None) return [getattr(obj, 'pk') for obj in self.cleaned_data['termination_b_ids']]
class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=ConsolePort.objects.all(), queryset=ConsolePort.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -118,7 +118,7 @@ class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=ConsoleServerPort.objects.all(), queryset=ConsoleServerPort.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -129,7 +129,7 @@ class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -140,7 +140,7 @@ class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -151,7 +151,7 @@ class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -163,7 +163,7 @@ class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=FrontPort.objects.all(), queryset=FrontPort.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -174,7 +174,7 @@ class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
class ConnectCableToRearPortForm(ConnectCableToDeviceForm): class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=RearPort.objects.all(), queryset=RearPort.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -186,7 +186,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm): class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
# Termination A # Termination A
termination_a_id = DynamicModelChoiceField( termination_a_ids = DynamicModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Side', label='Side',
disabled_indicator='_occupied' disabled_indicator='_occupied'
@ -231,7 +231,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
'site_id': '$termination_b_site', 'site_id': '$termination_b_site',
} }
) )
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=CircuitTermination.objects.all(), queryset=CircuitTermination.objects.all(),
label='Side', label='Side',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -242,8 +242,8 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
class Meta(ConnectCableToDeviceForm.Meta): class Meta(ConnectCableToDeviceForm.Meta):
fields = [ fields = [
'termination_a_id', 'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', 'termination_a_ids', 'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup',
'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type', 'status', 'tenant_group', 'termination_b_site', 'termination_b_circuit', 'termination_b_ids', 'type', 'status', 'tenant_group',
'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
] ]
@ -258,7 +258,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm): class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
# Termination A # Termination A
termination_a_id = DynamicModelChoiceField( termination_a_ids = DynamicModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied' disabled_indicator='_occupied'
@ -307,7 +307,7 @@ class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
'location_id': '$termination_b_location', 'location_id': '$termination_b_location',
} }
) )
termination_b_id = DynamicModelChoiceField( termination_b_ids = DynamicModelMultipleChoiceField(
queryset=PowerFeed.objects.all(), queryset=PowerFeed.objects.all(),
label='Name', label='Name',
disabled_indicator='_occupied', disabled_indicator='_occupied',
@ -318,8 +318,8 @@ class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
class Meta(ConnectCableToDeviceForm.Meta): class Meta(ConnectCableToDeviceForm.Meta):
fields = [ fields = [
'termination_a_id', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_a_ids', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', 'termination_b_location', 'termination_b_powerpanel', 'termination_b_ids', 'type', 'status', 'tenant_group',
'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
] ]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.0.4 on 2022-04-25 16:35
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0153_created_datetimefield'),
]
operations = [
migrations.AddField(
model_name='cable',
name='termination_a_ids',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveBigIntegerField(), null=True, size=None),
),
migrations.AddField(
model_name='cable',
name='termination_b_ids',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveBigIntegerField(), null=True, size=None),
),
]

View File

@ -0,0 +1,36 @@
from django.contrib.postgres.fields import ArrayField
from django.db import migrations
from django.db.models import ExpressionWrapper, F
def copy_termination_ids(apps, schema_editor):
"""
Copy original A & B termination ID values to new array fields.
"""
Cable = apps.get_model('dcim', 'Cable')
# TODO: Optimize data migration using F expressions
# Cable.objects.update(
# termination_a_ids=ExpressionWrapper(F('termination_a_id'), output_field=ArrayField),
# termination_b_ids=ExpressionWrapper(F('termination_b_id'), output_field=ArrayField)
# )
for cable in Cable.objects.all():
Cable.objects.filter(pk=cable.pk).update(
termination_a_ids=[cable.termination_a_id],
termination_b_ids=[cable.termination_b_id]
)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0154_cable_add_termination_id_arrays'),
]
operations = [
migrations.RunPython(
code=copy_termination_ids,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0.4 on 2022-04-25 20:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0155_cable_copy_termination_ids'),
]
operations = [
migrations.AlterUniqueTogether(
name='cable',
unique_together=set(),
),
migrations.RemoveField(
model_name='cable',
name='termination_a_id',
),
migrations.RemoveField(
model_name='cable',
name='termination_b_id',
),
]

View File

@ -2,6 +2,7 @@ from collections import defaultdict
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
@ -38,10 +39,9 @@ class Cable(NetBoxModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+' related_name='+'
) )
termination_a_id = models.PositiveBigIntegerField() termination_a_ids = ArrayField(
termination_a = GenericForeignKey( base_field=models.PositiveBigIntegerField(),
ct_field='termination_a_type', null=True
fk_field='termination_a_id'
) )
termination_b_type = models.ForeignKey( termination_b_type = models.ForeignKey(
to=ContentType, to=ContentType,
@ -49,10 +49,9 @@ class Cable(NetBoxModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+' related_name='+'
) )
termination_b_id = models.PositiveBigIntegerField() termination_b_ids = ArrayField(
termination_b = GenericForeignKey( base_field=models.PositiveBigIntegerField(),
ct_field='termination_b_type', null=True
fk_field='termination_b_id'
) )
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
@ -115,10 +114,6 @@ class Cable(NetBoxModel):
class Meta: class Meta:
ordering = ['pk'] ordering = ['pk']
unique_together = (
('termination_a_type', 'termination_a_id'),
('termination_b_type', 'termination_b_id'),
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -137,9 +132,9 @@ class Cable(NetBoxModel):
instance = super().from_db(db, field_names, values) instance = super().from_db(db, field_names, values)
instance._orig_termination_a_type_id = instance.termination_a_type_id instance._orig_termination_a_type_id = instance.termination_a_type_id
instance._orig_termination_a_id = instance.termination_a_id instance._orig_termination_a_ids = instance.termination_a_ids
instance._orig_termination_b_type_id = instance.termination_b_type_id instance._orig_termination_b_type_id = instance.termination_b_type_id
instance._orig_termination_b_id = instance.termination_b_id instance._orig_termination_b_ids = instance.termination_b_ids
return instance return instance
@ -150,6 +145,18 @@ class Cable(NetBoxModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:cable', args=[self.pk]) return reverse('dcim:cable', args=[self.pk])
@property
def termination_a(self):
if not hasattr(self, 'termination_a_type') or not self.termination_a_ids:
return []
return list(self.termination_a_type.model_class().objects.filter(pk__in=self.termination_a_ids))
@property
def termination_b(self):
if not hasattr(self, 'termination_b_type') or not self.termination_b_ids:
return []
return list(self.termination_b_type.model_class().objects.filter(pk__in=self.termination_b_ids))
def clean(self): def clean(self):
from circuits.models import CircuitTermination from circuits.models import CircuitTermination
@ -158,9 +165,8 @@ class Cable(NetBoxModel):
# Validate that termination A exists # Validate that termination A exists
if not hasattr(self, 'termination_a_type'): if not hasattr(self, 'termination_a_type'):
raise ValidationError('Termination A type has not been specified') raise ValidationError('Termination A type has not been specified')
try: model = self.termination_a_type.model_class()
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id) if model.objects.filter(pk__in=self.termination_a_ids).count() != len(self.termination_a_ids):
except ObjectDoesNotExist:
raise ValidationError({ raise ValidationError({
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type) 'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
}) })
@ -168,9 +174,8 @@ class Cable(NetBoxModel):
# Validate that termination B exists # Validate that termination B exists
if not hasattr(self, 'termination_b_type'): if not hasattr(self, 'termination_b_type'):
raise ValidationError('Termination B type has not been specified') raise ValidationError('Termination B type has not been specified')
try: model = self.termination_a_type.model_class()
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id) if model.objects.filter(pk__in=self.termination_b_ids).count() != len(self.termination_b_ids):
except ObjectDoesNotExist:
raise ValidationError({ raise ValidationError({
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type) 'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
}) })
@ -180,14 +185,14 @@ class Cable(NetBoxModel):
err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.' err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.'
if ( if (
self.termination_a_type_id != self._orig_termination_a_type_id or self.termination_a_type_id != self._orig_termination_a_type_id or
self.termination_a_id != self._orig_termination_a_id set(self.termination_a_ids) != set(self._orig_termination_a_ids)
): ):
raise ValidationError({ raise ValidationError({
'termination_a': err_msg 'termination_a': err_msg
}) })
if ( if (
self.termination_b_type_id != self._orig_termination_b_type_id or self.termination_b_type_id != self._orig_termination_b_type_id or
self.termination_b_id != self._orig_termination_b_id set(self.termination_b_ids) != set(self._orig_termination_b_ids)
): ):
raise ValidationError({ raise ValidationError({
'termination_b': err_msg 'termination_b': err_msg
@ -197,17 +202,17 @@ class Cable(NetBoxModel):
type_b = self.termination_b_type.model type_b = self.termination_b_type.model
# Validate interface types # Validate interface types
if type_a == 'interface' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES: if type_a == 'interface':
for term in self.termination_a:
if term.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({ raise ValidationError({
'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format( 'termination_a_id': f'Cables cannot be terminated to {term.get_type_display()} interfaces'
self.termination_a.get_type_display()
)
}) })
if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES: if type_a == 'interface':
for term in self.termination_b:
if term.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({ raise ValidationError({
'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format( 'termination_b_id': f'Cables cannot be terminated to {term.get_type_display()} interfaces'
self.termination_b.get_type_display()
)
}) })
# Check that termination types are compatible # Check that termination types are compatible
@ -216,50 +221,48 @@ class Cable(NetBoxModel):
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}" f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
) )
# Check that two connected RearPorts have the same number of positions (if both are >1) # TODO: Is this validation still necessary?
if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort): # # Check that two connected RearPorts have the same number of positions (if both are >1)
if self.termination_a.positions > 1 and self.termination_b.positions > 1: # if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
if self.termination_a.positions != self.termination_b.positions: # if self.termination_a.positions > 1 and self.termination_b.positions > 1:
raise ValidationError( # if self.termination_a.positions != self.termination_b.positions:
f"{self.termination_a} has {self.termination_a.positions} position(s) but " # raise ValidationError(
f"{self.termination_b} has {self.termination_b.positions}. " # f"{self.termination_a} has {self.termination_a.positions} position(s) but "
f"Both terminations must have the same number of positions (if greater than one)." # f"{self.termination_b} has {self.termination_b.positions}. "
) # f"Both terminations must have the same number of positions (if greater than one)."
# )
# A termination point cannot be connected to itself # A termination point cannot be connected to itself
if self.termination_a == self.termination_b: if set(self.termination_a).intersection(self.termination_b):
raise ValidationError(f"Cannot connect {self.termination_a_type} to itself") raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
# A front port cannot be connected to its corresponding rear port # TODO
if ( # # A front port cannot be connected to its corresponding rear port
type_a in ['frontport', 'rearport'] and # if (
type_b in ['frontport', 'rearport'] and # type_a in ['frontport', 'rearport'] and
( # type_b in ['frontport', 'rearport'] and
getattr(self.termination_a, 'rear_port', None) == self.termination_b or # (
getattr(self.termination_b, 'rear_port', None) == self.termination_a # getattr(self.termination_a, 'rear_port', None) == self.termination_b or
) # getattr(self.termination_b, 'rear_port', None) == self.termination_a
): # )
raise ValidationError("A front port cannot be connected to it corresponding rear port") # ):
# raise ValidationError("A front port cannot be connected to it corresponding rear port")
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable # TODO
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None: # # A CircuitTermination attached to a ProviderNetwork cannot have a Cable
raise ValidationError({ # if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
'termination_a_id': "Circuit terminations attached to a provider network may not be cabled." # raise ValidationError({
}) # 'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None: # })
raise ValidationError({ # if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
'termination_b_id': "Circuit terminations attached to a provider network may not be cabled." # raise ValidationError({
}) # 'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
# })
# Check for an existing Cable connected to either termination object # Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self): for term in [*self.termination_a, *self.termination_b]:
raise ValidationError("{} already has a cable attached (#{})".format( if term.cable not in (None, self):
self.termination_a, self.termination_a.cable_id raise ValidationError(f'{term} already has a cable attached (#{term.cable_id})')
))
if self.termination_b.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_b, self.termination_b.cable_id
))
# Validate length and length_unit # Validate length and length_unit
if self.length is not None and not self.length_unit: if self.length is not None and not self.length_unit:
@ -276,10 +279,10 @@ class Cable(NetBoxModel):
self._abs_length = None self._abs_length = None
# Store the parent Device for the A and B terminations (if applicable) to enable filtering # Store the parent Device for the A and B terminations (if applicable) to enable filtering
if hasattr(self.termination_a, 'device'): if hasattr(self.termination_a[0], 'device'):
self._termination_a_device = self.termination_a.device self._termination_a_device = self.termination_a[0].device
if hasattr(self.termination_b, 'device'): if hasattr(self.termination_b[0], 'device'):
self._termination_b_device = self.termination_b.device self._termination_b_device = self.termination_b[0].device
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -289,14 +292,6 @@ class Cable(NetBoxModel):
def get_status_color(self): def get_status_color(self):
return LinkStatusChoices.colors.get(self.status) return LinkStatusChoices.colors.get(self.status)
def get_compatible_types(self):
"""
Return all termination types compatible with termination A.
"""
if self.termination_a is None:
return
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
class CablePath(models.Model): class CablePath(models.Model):
""" """

View File

@ -79,21 +79,24 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
logger.debug(f"Skipping endpoint updates for imported cable {instance}") logger.debug(f"Skipping endpoint updates for imported cable {instance}")
return return
# Cache the Cable on its two termination points # TODO: Update link peer fields
if instance.termination_a.cable != instance: # Cache the Cable on its termination points
logger.debug(f"Updating termination A for cable {instance}") for term in instance.termination_a:
instance.termination_a.cable = instance if term.cable != instance:
instance.termination_a._link_peer = instance.termination_b logger.debug(f"Updating termination A for cable {instance}: {term}")
instance.termination_a.save() term.cable = instance
if instance.termination_b.cable != instance: # term._link_peer = instance.termination_b
term.save()
for term in instance.termination_b:
if term.cable != instance:
logger.debug(f"Updating termination B for cable {instance}") logger.debug(f"Updating termination B for cable {instance}")
instance.termination_b.cable = instance term.cable = instance
instance.termination_b._link_peer = instance.termination_a # term._link_peer = instance.termination_a
instance.termination_b.save() term.save()
# Create/update cable paths # Create/update cable paths
if created: if created:
for termination in (instance.termination_a, instance.termination_b): for termination in [*instance.termination_a, *instance.termination_b]:
if isinstance(termination, PathEndpoint): if isinstance(termination, PathEndpoint):
create_cablepath(termination) create_cablepath(termination)
else: else:
@ -116,14 +119,14 @@ def nullify_connected_endpoints(instance, **kwargs):
logger = logging.getLogger('netbox.dcim.cable') logger = logging.getLogger('netbox.dcim.cable')
# Disassociate the Cable from its termination points # Disassociate the Cable from its termination points
if instance.termination_a is not None: if instance.termination_a:
logger.debug(f"Nullifying termination A for cable {instance}") logger.debug(f"Nullifying termination A for cable {instance}")
model = instance.termination_a._meta.model model = instance.termination_a_type.model_class()
model.objects.filter(pk=instance.termination_a.pk).update(_link_peer_type=None, _link_peer_id=None) model.objects.filter(pk__in=instance.termination_a_ids).update(_link_peer_type=None, _link_peer_id=None)
if instance.termination_b is not None: if instance.termination_b:
logger.debug(f"Nullifying termination B for cable {instance}") logger.debug(f"Nullifying termination B for cable {instance}")
model = instance.termination_b._meta.model model = instance.termination_b_type.model_class()
model.objects.filter(pk=instance.termination_b.pk).update(_link_peer_type=None, _link_peer_id=None) model.objects.filter(pk__in=instance.termination_b_ids).update(_link_peer_type=None, _link_peer_id=None)
# Delete and retrace any dependent cable paths # Delete and retrace any dependent cable paths
for cablepath in CablePath.objects.filter(path__contains=instance): for cablepath in CablePath.objects.filter(path__contains=instance):

View File

@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
from dcim.models import Cable from dcim.models import Cable
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT from .template_code import CABLE_LENGTH, CABLE_TERMINATION, CABLE_TERMINATION_PARENT
__all__ = ( __all__ = (
'CableTable', 'CableTable',
@ -28,7 +28,8 @@ class CableTable(NetBoxTable):
linkify=True, linkify=True,
verbose_name='Rack A' verbose_name='Rack A'
) )
termination_a = tables.Column( termination_a = tables.TemplateColumn(
template_code=CABLE_TERMINATION,
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
linkify=True, linkify=True,
@ -46,7 +47,8 @@ class CableTable(NetBoxTable):
linkify=True, linkify=True,
verbose_name='Rack B' verbose_name='Rack B'
) )
termination_b = tables.Column( termination_b = tables.TemplateColumn(
template_code=CABLE_TERMINATION,
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
linkify=True, linkify=True,

View File

@ -13,14 +13,20 @@ CABLE_LENGTH = """
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %} {% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
""" """
CABLE_TERMINATION = """
{{ value|join:", " }}
"""
CABLE_TERMINATION_PARENT = """ CABLE_TERMINATION_PARENT = """
{% if value.device %} {% with value.0 as termination %}
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a> {% if termination.device %}
{% elif value.circuit %} <a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
<a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a> {% elif termination.circuit %}
{% elif value.power_panel %} <a href="{{ termination.circuit.get_absolute_url }}">{{ termination.circuit }}</a>
<a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a> {% elif termination.power_panel %}
<a href="{{ termination.power_panel.get_absolute_url }}">{{ termination.power_panel }}</a>
{% endif %} {% endif %}
{% endwith %}
""" """
DEVICE_LINK = """ DEVICE_LINK = """

View File

@ -2831,12 +2831,13 @@ class CableCreateView(generic.ObjectEditView):
def alter_object(self, obj, request, url_args, url_kwargs): def alter_object(self, obj, request, url_args, url_kwargs):
termination_a_type = url_kwargs.get('termination_a_type') termination_a_type = url_kwargs.get('termination_a_type')
termination_a_id = request.GET.get('termination_a_id') termination_a_ids = request.GET.get('termination_a_ids', [])
app_label, model = request.GET.get('termination_b_type').split('.') app_label, model = request.GET.get('termination_b_type').split('.')
self.termination_b_type = ContentType.objects.get(app_label=app_label, model=model) self.termination_b_type = ContentType.objects.get(app_label=app_label, model=model)
# Initialize Cable termination attributes # Initialize Cable termination attributes
obj.termination_a = termination_a_type.objects.get(pk=termination_a_id) obj.termination_a_type = ContentType.objects.get_for_model(termination_a_type)
obj.termination_a_ids = termination_a_type.objects.filter(pk__in=termination_a_ids)
obj.termination_b_type = self.termination_b_type obj.termination_b_type = self.termination_b_type
return obj return obj
@ -2844,21 +2845,19 @@ class CableCreateView(generic.ObjectEditView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
obj = self.get_object(**kwargs) obj = self.get_object(**kwargs)
obj = self.alter_object(obj, request, args, kwargs) obj = self.alter_object(obj, request, args, kwargs)
initial_data = request.GET
# Parse initial data manually to avoid setting field values as lists # TODO
initial_data = {k: request.GET[k] for k in request.GET} # # Set initial site and rack based on side A termination (if not already set)
# termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
# Set initial site and rack based on side A termination (if not already set) # if 'termination_b_site' not in initial_data:
termination_a_site = getattr(obj.termination_a.parent_object, 'site', None) # initial_data['termination_b_site'] = termination_a_site
if 'termination_b_site' not in initial_data: # if 'termination_b_rack' not in initial_data:
initial_data['termination_b_site'] = termination_a_site # initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
if 'termination_b_rack' not in initial_data:
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
form = self.form(instance=obj, initial=initial_data) form = self.form(instance=obj, initial=initial_data)
# Set the queryset of termination A # Set the queryset of termination A
form.fields['termination_a_id'].queryset = kwargs['termination_a_type'].objects.all() form.fields['termination_a_ids'].queryset = kwargs['termination_a_type'].objects.all()
return render(request, self.template_name, { return render(request, self.template_name, {
'obj': obj, 'obj': obj,

View File

@ -8,9 +8,7 @@
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Cable</h5>
Cable
</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
@ -63,17 +61,13 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Termination A</h5>
Termination A
</h5>
<div class="card-body"> <div class="card-body">
{% include 'dcim/inc/cable_termination.html' with termination=object.termination_a %} {% include 'dcim/inc/cable_termination.html' with termination=object.termination_a %}
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Termination B</h5>
Termination B
</h5>
<div class="card-body"> <div class="card-body">
{% include 'dcim/inc/cable_termination.html' with termination=object.termination_b %} {% include 'dcim/inc/cable_termination.html' with termination=object.termination_b %}
</div> </div>

View File

@ -3,7 +3,7 @@
{% load helpers %} {% load helpers %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Connect {{ form.instance.termination_a.device }} {{ form.instance.termination_a }} to {{ termination_b_type|bettertitle }}{% endblock %} {% block title %}Connect Cable to {{ termination_b_type|bettertitle }}{% endblock %}
{% block tabs %} {% block tabs %}
<ul class="nav nav-tabs px-3"> <ul class="nav nav-tabs px-3">
@ -15,7 +15,7 @@
{% block content-wrapper %} {% block content-wrapper %}
<div class="tab-content"> <div class="tab-content">
{% with termination_a=form.instance.termination_a %} {% with termination_a=form.instance.termination_a.0 %}
{% render_errors form %} {% render_errors form %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@ -92,7 +92,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% render_field form.termination_a_id %} {% render_field form.termination_a_ids %}
</div> </div>
</div> </div>
</div> </div>
@ -148,7 +148,7 @@
<input class="form-control" value="{{ termination_b_type|capfirst }}" disabled /> <input class="form-control" value="{{ termination_b_type|capfirst }}" disabled />
</div> </div>
</div> </div>
{% render_field form.termination_b_id %} {% render_field form.termination_b_ids %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,42 +1,46 @@
{% load helpers %} {% load helpers %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
{% if termination.device %} {% if termination.0.device %}
{# Device component #} {# Device component #}
<tr> <tr>
<td>Device</td> <td>Device</td>
<td>{{ termination.device|linkify }}</td> <td>{{ termination.0.device|linkify }}</td>
</tr> </tr>
<tr> <tr>
<td>Site</td> <td>Site</td>
<td>{{ termination.device.site|linkify }}</td> <td>{{ termination.0.device.site|linkify }}</td>
</tr> </tr>
{% if termination.device.rack %} {% if termination.0.device.rack %}
<tr> <tr>
<td>Rack</td> <td>Rack</td>
<td>{{ termination.device.rack|linkify }}</td> <td>{{ termination.0.device.rack|linkify }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<td>Type</td> <td>Type</td>
<td>{{ termination|meta:"verbose_name"|capfirst }}</td> <td>{{ termination.0|meta:"verbose_name"|capfirst }}</td>
</tr> </tr>
<tr> <tr>
<td>Component</td> <td>Component(s)</td>
<td>{{ termination|linkify }}</td> <td>
{% for term in termination %}
{{ term|linkify }}{% if not forloop.last %},{% endif %}
{% endfor %}
</td>
</tr> </tr>
{% else %} {% else %}
{# Circuit termination #} {# Circuit termination #}
<tr> <tr>
<td>Provider</td> <td>Provider</td>
<td>{{ termination.circuit.provider|linkify }}</td> <td>{{ termination.0.circuit.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<td>Circuit</td> <td>Circuit</td>
<td>{{ termination.circuit|linkify }}</td> <td>
</tr> {% for term in termination %}
<tr> {{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %}
<td>Termination</td> {% endfor %}
<td>{{ termination }}</td> </td>
</tr> </tr>
{% endif %} {% endif %}
</table> </table>

View File

@ -132,6 +132,7 @@ def serialize_object(obj, extra=None):
implicitly excluded. implicitly excluded.
""" """
json_str = serialize('json', [obj]) json_str = serialize('json', [obj])
print(json_str)
data = json.loads(json_str)[0]['fields'] data = json.loads(json_str)[0]['fields']
# Exclude any MPTTModel fields # Exclude any MPTTModel fields