Adding trace information for quick display

This commit is contained in:
Sander Steffann 2019-10-19 22:55:07 +02:00
parent 857f27c370
commit 84c9ef8144
7 changed files with 366 additions and 76 deletions

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.5 on 2019-10-09 14:56
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('circuits', '0015_custom_tag_models'),
]
operations = [
migrations.RenameField(
model_name='circuittermination',
old_name='connected_endpoint',
new_name='old_connected_endpoint',
),
migrations.AddField(
model_name='circuittermination',
name='connected_endpoint_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='connected_endpoint_type',
field=models.ForeignKey(blank=True, limit_choices_to={
'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport',
'rearport', 'circuittermination']
}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='circuittermination',
name='_trace',
field=django.contrib.postgres.fields.jsonb.JSONField(default=list),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.5 on 2019-10-09 19:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0016_generic_connected_endpoint'),
('dcim', '0077_migrate_connected_endpoint'),
]
operations = [
migrations.RemoveField(
model_name='circuittermination',
name='old_connected_endpoint',
),
]

View File

@ -1,9 +1,11 @@
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES, CABLE_TERMINATION_TYPES
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem from extras.models import CustomFieldModel, ObjectChange, TaggedItem
@ -228,13 +230,26 @@ class CircuitTermination(CableTermination):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuit_terminations' related_name='circuit_terminations'
) )
connected_endpoint = models.OneToOneField( connected_endpoint_type = models.ForeignKey(
to='dcim.Interface', to=ContentType,
on_delete=models.SET_NULL, limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
on_delete=models.PROTECT,
related_name='+', related_name='+',
blank=True, blank=True,
null=True null=True
) )
connected_endpoint_id = models.PositiveIntegerField(
blank=True,
null=True
)
connected_endpoint = GenericForeignKey(
ct_field='connected_endpoint_type',
fk_field='connected_endpoint_id'
)
_trace = JSONField(
default=list
)
connection_status = models.NullBooleanField( connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
blank=True blank=True
@ -298,7 +313,11 @@ class CircuitTermination(CableTermination):
return None return None
def get_peer_port(self): def get_peer_port(self):
peer_termination = self.get_peer_termination() return self.get_peer_termination()
if peer_termination is None:
return None def get_endpoint_attributes(self):
return peer_termination return {
**super().get_endpoint_attributes(),
'cid': self.circuit.cid,
'site': self.site.name,
}

View File

@ -1,5 +1,6 @@
# Generated by Django 2.2.5 on 2019-10-06 19:01 # Generated by Django 2.2.5 on 2019-10-06 19:01
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -24,4 +25,9 @@ class Migration(migrations.Migration):
'rearport', 'circuittermination'] 'rearport', 'circuittermination']
}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'), }, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
), ),
migrations.AddField(
model_name='interface',
name='_trace',
field=django.contrib.postgres.fields.jsonb.JSONField(default=list),
),
] ]

View File

@ -1,61 +1,220 @@
# Generated by Django 2.2.5 on 2019-10-06 19:01 # Generated by Django 2.2.5 on 2019-10-06 19:01
from itertools import chain
from django.db import migrations from django.db import migrations
def connected_interface_to_endpoint(apps, schema_editor): def migration_trace(apps, endpoint, cable_history=None):
"""
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
[
(termination A, cable, termination B),
(termination C, cable, termination D),
(termination E, cable, termination F)
]
This is a stand-alone version of Interface.trace() that is safe to use in migrations.
"""
def get_generic_foreign_key(model_type, object_id):
if model_type is None or object_id is None:
return None
object_model = apps.get_model(model_type.app_label, model_type.model)
return object_model.objects.filter(pk=object_id).first()
circuittermination_model = apps.get_model('circuits', 'CircuitTermination')
frontport_model = apps.get_model('dcim', 'FrontPort')
rearport_model = apps.get_model('dcim', 'RearPort')
if not endpoint.cable:
return [(endpoint, None, None)]
# Record cable history to detect loops
if cable_history is None:
cable_history = []
elif endpoint.cable in cable_history:
return None
cable_history.append(endpoint.cable)
termination_a = get_generic_foreign_key(endpoint.cable.termination_a_type, endpoint.cable.termination_a_id)
termination_b = get_generic_foreign_key(endpoint.cable.termination_b_type, endpoint.cable.termination_b_id)
far_end = termination_b if termination_a == endpoint else termination_a
path = [(endpoint, endpoint.cable, far_end)]
if isinstance(far_end, circuittermination_model):
peer_side = 'Z' if far_end.term_side == 'A' else 'A'
peer_port = circuittermination_model.objects.filter(
circuit=far_end.circuit,
term_side=peer_side
).first()
elif isinstance(far_end, frontport_model):
peer_port = far_end.rear_port
elif isinstance(far_end, rearport_model):
peer_port = frontport_model.objects.filter(
rear_port=far_end,
rear_port_position=1,
).first()
else:
peer_port = None
if isinstance(far_end, rearport_model) and far_end.positions > 1:
# When we end up here we have a rear port with multiple front ports, and we don't know which front port
# to continue with. So this is the end of the line
return path
while peer_port:
path += migration_trace(apps, peer_port, cable_history)
if isinstance(far_end, frontport_model) and isinstance(peer_port, rearport_model) and peer_port.positions > 1:
# Trace the rear port separately, then continue with the corresponding front port
saved_rear_port_position = far_end.rear_port_position
far_end = path[-1][2]
peer_port = None
if isinstance(far_end, rearport_model):
# The trace ends with a rear port, find the corresponding front port
peer_port = frontport_model.objects.filter(
rear_port=far_end,
rear_port_position=saved_rear_port_position,
).first()
else:
# Everything else has already been handled by simple recursion
peer_port = None
return path
def migration_get_endpoint_attributes(endpoint):
"""
This is a stand-alone version of CableTermination.get_endpoint_attributes() that is safe to use in migrations.
"""
if not endpoint:
return {}
# We can't use isinstance because migrations give us fake classes
attributes = {
'id': endpoint.pk,
'type': endpoint.__class__.__name__,
}
if endpoint.__class__.__name__ == 'CircuitTermination':
attributes['cid'] = endpoint.circuit.cid
attributes['site'] = endpoint.site.name
elif endpoint.__class__.__name__ in ('Interface', 'FrontPort', 'RearPort'):
attributes['name'] = endpoint.name
parent = endpoint.device or endpoint.virtual_machine
if parent.name:
attributes['device'] = parent.name
attributes['device_id'] = parent.pk
elif parent.virtual_chassis and parent.virtual_chassis.master.name:
attributes['device'] = "{}:{}".format(parent.virtual_chassis.master, parent.vc_position)
attributes['device_id'] = parent.virtual_chassis.master.pk
elif hasattr(parent, 'device_type'):
attributes['device'] = "{}".format(parent.device_type)
attributes['device_id'] = parent.pk
else:
attributes['device'] = ""
attributes['site'] = parent.site.name
return attributes
def to_generic_connected_endpoint(apps, schema_editor):
print("\nReconstructing all endpoints...", end='')
interface_model = apps.get_model('dcim', 'Interface') interface_model = apps.get_model('dcim', 'Interface')
circuittermination_model = apps.get_model('circuits', 'CircuitTermination')
contenttype_model = apps.get_model('contenttypes', 'ContentType') contenttype_model = apps.get_model('contenttypes', 'ContentType')
db_alias = schema_editor.connection.alias
model_type = contenttype_model.objects.get_for_model(interface_model) interface_endpoints = interface_model.objects.using(db_alias).all()
for interface in interface_model.objects.exclude(_connected_interface=None): circuittermination_endpoints = circuittermination_model.objects.using(db_alias).all()
interface.connected_endpoint_type = model_type for endpoint in chain(interface_endpoints, circuittermination_endpoints):
interface.connected_endpoint_id = interface._connected_interface.pk path = migration_trace(apps, endpoint)
interface.save()
# The trace returns left and right, we just want a single list
# We also want to skip the first endpoint, which is the starting point itself
endpoints = [
item for sublist in (
[left, right] for left, cable, right in path
)
for item in sublist if item
][1:]
if endpoints:
model_type = contenttype_model.objects.get_for_model(endpoints[-1])
endpoint.connected_endpoint_type = model_type
endpoint.connected_endpoint_id = endpoints[-1].pk
else:
endpoint.connected_endpoint_type = None
endpoint.connected_endpoint_id = None
endpoint._trace = [migration_get_endpoint_attributes(endpoint) for endpoint in endpoints]
endpoint.save()
print(".", end='', flush=True)
def connected_endpoint_to_interface(apps, schema_editor): def from_generic_connected_endpoint(apps, schema_editor):
interface_model = apps.get_model('dcim', 'Interface') db_alias = schema_editor.connection.alias
contenttype_model = apps.get_model('contenttypes', 'ContentType')
model_type = contenttype_model.objects.get_for_model(interface_model)
for interface in interface_model.objects.filter(connected_endpoint_type=model_type):
interface._connected_interface = interface.connected_endpoint
interface.save()
def connected_circuittermination_to_endpoint(apps, schema_editor):
interface_model = apps.get_model('dcim', 'Interface') interface_model = apps.get_model('dcim', 'Interface')
contenttype_model = apps.get_model('contenttypes', 'ContentType') contenttype_model = apps.get_model('contenttypes', 'ContentType')
circuittermination_model = apps.get_model('circuits', 'CircuitTermination') circuittermination_model = apps.get_model('circuits', 'CircuitTermination')
model_type = contenttype_model.objects.get_for_model(circuittermination_model) print("\nReverting interface endpoints in interfaces...", end='')
for interface in interface_model.objects.exclude(_connected_circuittermination=None):
interface.connected_endpoint_type = model_type
interface.connected_endpoint_id = interface._connected_circuittermination.pk
interface.save()
model_type = contenttype_model.objects.get_for_model(interface_model)
for interface in interface_model.objects.using(db_alias).filter(connected_endpoint_type=model_type):
try:
interface._connected_interface_id = interface.connected_endpoint_id
interface.save()
except interface_model.DoesNotExist:
# Dangling generic foreign key
pass
def connected_endpoint_to_circuittermination(apps, schema_editor): print(".", end='', flush=True)
interface_model = apps.get_model('dcim', 'Interface')
contenttype_model = apps.get_model('contenttypes', 'ContentType') print("\nReverting circuit termination endpoints in interfaces...", end='')
circuittermination_model = apps.get_model('circuits', 'CircuitTermination')
model_type = contenttype_model.objects.get_for_model(circuittermination_model) model_type = contenttype_model.objects.get_for_model(circuittermination_model)
for interface in interface_model.objects.filter(connected_endpoint_type=model_type): for interface in interface_model.objects.using(db_alias).filter(connected_endpoint_type=model_type):
interface._connected_circuittermination = interface.connected_endpoint try:
interface.save() interface._connected_circuittermination_id = interface.connected_endpoint_id
interface.save()
except circuittermination_model.DoesNotExist:
# Dangling generic foreign key
pass
print(".", end='', flush=True)
print("\nReverting circuit termination endpoints...", end='')
model_type = contenttype_model.objects.get_for_model(interface_model)
for interface in circuittermination_model.objects.using(db_alias).filter(connected_endpoint_type=model_type):
try:
interface.old_connected_interface_id = interface.connected_endpoint_id
interface.save()
except interface_model.DoesNotExist:
# Dangling generic foreign key
pass
print(".", end='', flush=True)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('circuits', '0016_generic_connected_endpoint'),
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('dcim', '0076_add_generic_connected_endpoint'), ('dcim', '0076_add_generic_connected_endpoint'),
] ]
operations = [ operations = [
migrations.RunPython(connected_interface_to_endpoint, migrations.RunPython(to_generic_connected_endpoint,
connected_endpoint_to_interface), from_generic_connected_endpoint),
migrations.RunPython(connected_circuittermination_to_endpoint,
connected_endpoint_to_circuittermination),
] ]

View File

@ -160,6 +160,11 @@ class CableTermination(models.Model):
if self._cabled_as_b.exists(): if self._cabled_as_b.exists():
return self.cable.termination_a return self.cable.termination_a
def get_endpoint_attributes(self):
return {
'id': self.pk,
'type': self.__class__.__name__,
}
# #
# Regions # Regions
@ -2168,6 +2173,9 @@ class Interface(CableTermination, ComponentModel):
ct_field='connected_endpoint_type', ct_field='connected_endpoint_type',
fk_field='connected_endpoint_id' fk_field='connected_endpoint_id'
) )
_trace = JSONField(
default=list
)
connected_interface = GenericRelation( connected_interface = GenericRelation(
to='self', to='self',
@ -2382,6 +2390,15 @@ class Interface(CableTermination, ComponentModel):
def count_ipaddresses(self): def count_ipaddresses(self):
return self.ip_addresses.count() return self.ip_addresses.count()
def get_endpoint_attributes(self):
return {
**super().get_endpoint_attributes(),
'name': self.name,
'device': self.parent.display_name,
'device_id': self.parent.pk,
'site': self.parent.site.name,
}
# #
# Pass-through ports # Pass-through ports
@ -2456,6 +2473,15 @@ class FrontPort(CableTermination, ComponentModel):
def get_peer_port(self): def get_peer_port(self):
return self.rear_port return self.rear_port
def get_endpoint_attributes(self):
return {
**super().get_endpoint_attributes(),
'name': self.name,
'device': self.parent.display_name,
'device_id': self.parent.pk,
'site': self.parent.site.name,
}
class RearPort(CableTermination, ComponentModel): class RearPort(CableTermination, ComponentModel):
""" """
@ -2513,6 +2539,15 @@ class RearPort(CableTermination, ComponentModel):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
def get_endpoint_attributes(self):
return {
**super().get_endpoint_attributes(),
'name': self.name,
'device': self.parent.display_name,
'device_id': self.parent.pk,
'site': self.parent.site.name,
}
# #
# Device bays # Device bays
@ -2933,31 +2968,34 @@ class Cable(ChangeLoggedModel):
return return
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name] return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
def get_path_endpoints(self): def get_related_endpoints(self):
""" """
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be Traverse both ends of a cable path and return a list of all related endpoints.
None.
""" """
# Termination points trace from themselves, through the cable and beyond. Tracing from the B termination # Termination points trace from themselves, through the cable and beyond. Tracing from the B termination
# therefore traces in the direction of A [(termination_b, cable, termination_a), (...)] and vice versa. # therefore traces in the direction of A [(termination_b, cable, termination_a), (...)] and vice versa.
# Every path therefore also has at least one segment (the current cable). # Every path therefore also has at least one segment (the current cable).
a_path = self.termination_b.trace() paths = [
b_path = self.termination_a.trace() self.termination_b.trace(),
self.termination_a.trace(),
]
# Determine overall path status (connected or planned) # Use a dict here to avoid storing duplicates. The same object retrieved twice will have different identities.
if self.status == CONNECTION_STATUS_PLANNED: endpoints = {}
path_status = CONNECTION_STATUS_PLANNED while paths:
else: path = paths.pop()
path_status = CONNECTION_STATUS_CONNECTED for left, cable, right in path:
for segment in a_path[1:] + b_path[1:]: if right is not None:
if segment[1] is None or segment[1].status == CONNECTION_STATUS_PLANNED: key = '{cls}-{pk}'.format(cls=right.__class__.__name__, pk=right.pk)
path_status = CONNECTION_STATUS_PLANNED endpoints[key] = right
break
a_endpoint = a_path[-1][2] # If a path ends in a RearPort, then everything connected through its FrontPorts is related as well
b_endpoint = b_path[-1][2] if isinstance(path[-1][2], RearPort):
front_ports = FrontPort.objects.filter(rear_port=path[-1][2])
for front_port in front_ports:
paths.append(front_port.trace())
return a_endpoint, b_endpoint, path_status return list(endpoints.values())
# #

View File

@ -43,15 +43,9 @@ def update_connected_endpoints(instance, **kwargs):
instance.termination_b.cable = instance instance.termination_b.cable = instance
instance.termination_b.save() instance.termination_b.save()
# Check if this Cable has formed a complete path. If so, update both endpoints. # Update all endpoints affected by this cable
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints() endpoints = instance.get_related_endpoints()
if endpoint_a is not None and endpoint_b is not None: update_endpoints(endpoints)
endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status
endpoint_a.save()
endpoint_b.connected_endpoint = endpoint_a
endpoint_b.connection_status = path_status
endpoint_b.save()
@receiver(pre_delete, sender=Cable) @receiver(pre_delete, sender=Cable)
@ -59,7 +53,7 @@ def nullify_connected_endpoints(instance, **kwargs):
""" """
When a Cable is deleted, check for and update its two connected endpoints When a Cable is deleted, check for and update its two connected endpoints
""" """
endpoint_a, endpoint_b, _ = instance.get_path_endpoints() endpoints = instance.get_related_endpoints()
# 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 is not None:
@ -69,11 +63,29 @@ def nullify_connected_endpoints(instance, **kwargs):
instance.termination_b.cable = None instance.termination_b.cable = None
instance.termination_b.save() instance.termination_b.save()
# If this Cable was part of a complete path, tear it down # Update all endpoints affected by this cable
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'): update_endpoints(endpoints)
endpoint_a.connected_endpoint = None
endpoint_a.connection_status = None
endpoint_a.save() def update_endpoints(endpoints):
endpoint_b.connected_endpoint = None """
endpoint_b.connection_status = None Update all endpoints affected by this cable
endpoint_b.save() """
for endpoint in endpoints:
if not hasattr(endpoint, 'connected_endpoint'):
continue
path = endpoint.trace()
# The trace returns left and right, we just want a single list
# We also want to skip the first endpoint, which is the starting point itself
endpoints = [
item for sublist in (
[left, right] for left, cable, right in path
)
for item in sublist if item
][1:]
endpoint.connected_endpoint = endpoints[-1] if endpoints else None
endpoint._trace = [endpoint.get_endpoint_attributes() for endpoint in endpoints]
endpoint.save()