Update cable tracing logic

This commit is contained in:
jeremystretch 2022-05-03 16:30:39 -04:00
parent 82706eb3a6
commit f0b722b0a5
3 changed files with 156 additions and 92 deletions

View File

@ -16,7 +16,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('cable_end', models.CharField(max_length=1)), ('cable_end', models.CharField(max_length=1)),
('termination_id', models.PositiveBigIntegerField()), ('termination_id', models.PositiveBigIntegerField()),
('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')), ('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.cable')),
('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), ('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
], ],
options={ options={

View File

@ -92,10 +92,12 @@ class Cable(NetBoxModel):
null=True null=True
) )
terminations = []
class Meta: class Meta:
ordering = ('pk',) ordering = ('pk',)
def __init__(self, *args, **kwargs): def __init__(self, *args, terminations=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# A copy of the PK to be used by __str__ in case the object is deleted # A copy of the PK to be used by __str__ in case the object is deleted
@ -104,19 +106,29 @@ class Cable(NetBoxModel):
# Cache the original status so we can check later if it's been changed # Cache the original status so we can check later if it's been changed
self._orig_status = self.status self._orig_status = self.status
# @classmethod # Assign associated CableTerminations (if any)
# def from_db(cls, db, field_names, values): if terminations:
# """ assert type(terminations) is list
# Cache the original A and B terminations of existing Cable instances for later reference inside clean(). assert self.pk is None
# """ for t in terminations:
# instance = super().from_db(db, field_names, values) t.cable = self
# self.terminations.append(t)
# instance._orig_termination_a_type_id = instance.termination_a_type_id
# instance._orig_termination_a_ids = instance.termination_a_ids @classmethod
# instance._orig_termination_b_type_id = instance.termination_b_type_id def from_db(cls, db, field_names, values):
# instance._orig_termination_b_ids = instance.termination_b_ids """
# Cache the original A and B terminations of existing Cable instances for later reference inside clean().
# return instance """
instance = super().from_db(db, field_names, values)
instance.terminations = CableTermination.objects.filter(cable=instance)
# instance._orig_termination_a_type_id = instance.termination_a_type_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_ids = instance.termination_b_ids
return instance
def __str__(self): def __str__(self):
pk = self.pk or self._pk pk = self.pk or self._pk
@ -186,7 +198,7 @@ class CableTermination(models.Model):
cable = models.ForeignKey( cable = models.ForeignKey(
to='dcim.Cable', to='dcim.Cable',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='terminations' related_name='+'
) )
cable_end = models.CharField( cable_end = models.CharField(
max_length=1, max_length=1,
@ -296,59 +308,80 @@ class CablePath(models.Model):
return f"Path #{self.pk}: {len(self.path)} nodes{status}" return f"Path #{self.pk}: {len(self.path)} nodes{status}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Save the flattened nodes list # Save the flattened nodes list
self._nodes = flatten_path(self.path) self._nodes = flatten_path(self.path)
# TODO super().save(*args, **kwargs)
# Record a direct reference to this CablePath on its originating object
# model = self.origin._meta.model # Record a direct reference to this CablePath on its originating object(s)
# model.objects.filter(pk=self.origin.pk).update(_path=self.pk) origins = [path_node_to_object(n) for n in self.path[0]]
origin_model = origins[0]._meta.model
origin_ids = [o.id for o in origins]
origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
@property @property
def segment_count(self): def segment_count(self):
return int(len(self.path) / 3) return int(len(self.path) / 3)
@classmethod @classmethod
def from_origin(cls, origin): def from_origin(cls, terminations):
""" """
Create a new CablePath instance as traced from the given path origin. Create a new CablePath instance as traced from the given path origin.
""" """
from circuits.models import CircuitTermination from circuits.models import CircuitTermination
if origin is None or origin.link is None: if not terminations or terminations[0].termination.link is None:
return None return None
destination = None
path = [] path = []
position_stack = [] position_stack = []
is_active = True is_active = True
is_split = False is_split = False
node = origin # Start building the path from its originating CableTerminations
path.append([
object_to_path_node(t.termination) for t in terminations
])
node = terminations[0].termination
while node.link is not None: while node.link is not None:
if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED: if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED:
is_active = False is_active = False
# Append the cable
path.append([object_to_path_node(node.link)])
# Follow the link to its far-end termination # Follow the link to its far-end termination
path.append(object_to_path_node(node.link)) if terminations[0].cable_end == 'A':
peer_termination = node.get_link_peer() peer_terminations = CableTermination.objects.filter(cable=terminations[0].cable, cable_end='B')
else:
peer_terminations = CableTermination.objects.filter(cable=terminations[0].cable, cable_end='A')
# Follow a FrontPort to its corresponding RearPort # Follow FrontPorts to their corresponding RearPorts
if isinstance(peer_termination, FrontPort): if isinstance(peer_terminations[0].termination, FrontPort):
path.append(object_to_path_node(peer_termination)) path.append([
node = peer_termination.rear_port object_to_path_node(t.termination) for t in peer_terminations
])
terminations = CableTermination.objects.filter(
termination_type=ContentType.objects.get_for_model(RearPort),
termination_id__in=[t.termination_id for t in peer_terminations]
)
node = terminations[0].termination
if node.positions > 1: if node.positions > 1:
position_stack.append(peer_termination.rear_port_position) position_stack.append(node.rear_port_position)
path.append(object_to_path_node(node)) path.append([
object_to_path_node(t.termination) for t in terminations
])
# Follow a RearPort to its corresponding FrontPort (if any) # Follow RearPorts to their corresponding FrontPorts (if any)
elif isinstance(peer_termination, RearPort): elif isinstance(peer_terminations[0], RearPort):
path.append(object_to_path_node(peer_termination)) path.append([
object_to_path_node(t.termination) for t in peer_terminations
])
# Determine the peer FrontPort's position # Determine the peer FrontPort's position
if peer_termination.positions == 1: if peer_terminations[0].termination.positions == 1:
position = 1 position = 1
elif position_stack: elif position_stack:
position = position_stack.pop() position = position_stack.pop()
@ -357,41 +390,55 @@ class CablePath(models.Model):
is_split = True is_split = True
break break
try: # Map FrontPorts to their corresponding RearPorts
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position) terminations = FrontPort.objects.filter(
path.append(object_to_path_node(node)) rear_port_id__in=[t.rear_port_id for t in peer_terminations],
except ObjectDoesNotExist: rear_port_position=position
# No corresponding FrontPort found for the RearPort )
break if terminations:
path.append([
object_to_path_node(t.termination) for t in terminations
])
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa) # Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
elif isinstance(peer_termination, CircuitTermination): elif isinstance(peer_terminations[0], CircuitTermination):
path.append(object_to_path_node(peer_termination)) path.append([
# Get peer CircuitTermination object_to_path_node(t.termination) for t in peer_terminations
node = peer_termination.get_peer_termination() ])
if node:
path.append(object_to_path_node(node)) # Get peer CircuitTerminations
if node.provider_network: term_side = 'Z' if peer_terminations[0].termination == 'A' else 'Z'
destination = node.provider_network terminations = CircuitTermination.objects.filter(
break circuit=peer_terminations[0].circuit,
elif node.site and not node.cable: term_side=term_side
destination = node.site )
break # Tracing across multiple circuits not currently supported
if len(terminations) > 1:
is_split = True
break
elif terminations:
path.append([
object_to_path_node(t.termination) for t in terminations
])
# TODO
# if node.provider_network:
# destination = node.provider_network
# break
# elif node.site and not node.cable:
# destination = node.site
# break
else: else:
# No peer CircuitTermination exists; halt the trace # No peer CircuitTermination exists; halt the trace
break break
# Anything else marks the end of the path # Anything else marks the end of the path
else: else:
destination = peer_termination path.append([
object_to_path_node(t.termination) for t in peer_terminations
])
break break
if destination is None:
is_active = False
return cls( return cls(
origin=origin,
destination=destination,
path=path, path=path,
is_active=is_active, is_active=is_active,
is_split=is_split is_split=is_split

View File

@ -79,15 +79,30 @@ 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
# TODO: Update link peer fields # Save any new CableTerminations
CableTermination.objects.bulk_create([
term for term in instance.terminations if not term.pk
])
# # Create/update cable paths # TODO: Update link peers
# if created: # Set cable on terminating endpoints
# for term in instance.terminations.all(): _terms = {
# if isinstance(term.termination, PathEndpoint): 'A': [],
# create_cablepath(term.termination) 'B': [],
# else: }
# rebuild_paths(term.termination) for term in instance.terminations:
if term.termination.cable != instance:
term.termination.cable = instance
term.termination.save()
_terms[term.cable_end].append(term)
# Create/update cable paths
if created:
for terms in _terms.values():
if isinstance(terms[0].termination, PathEndpoint):
create_cablepath(terms)
else:
rebuild_paths(terms)
# elif instance.status != instance._orig_status: # elif instance.status != instance._orig_status:
# # We currently don't support modifying either termination of an existing Cable. (This # # We currently don't support modifying either termination of an existing Cable. (This
# # may change in the future.) However, we do need to capture status changes and update # # may change in the future.) However, we do need to capture status changes and update
@ -98,17 +113,17 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
# rebuild_paths(instance) # rebuild_paths(instance)
@receiver(post_save, sender=CableTermination) # @receiver(post_save, sender=CableTermination)
def cache_cable_on_endpoints(instance, created, raw=False, **kwargs): # def cache_cable_on_endpoints(instance, created, raw=False, **kwargs):
if not raw: # if not raw:
model = instance.termination_type.model_class() # model = instance.termination_type.model_class()
model.objects.filter(pk=instance.termination_id).update(cable=instance.cable) # model.objects.filter(pk=instance.termination_id).update(cable=instance.cable)
#
#
@receiver(post_delete, sender=CableTermination) # @receiver(post_delete, sender=CableTermination)
def clear_cable_on_endpoints(instance, **kwargs): # def clear_cable_on_endpoints(instance, **kwargs):
model = instance.termination_type.model_class() # model = instance.termination_type.model_class()
model.objects.filter(pk=instance.termination_id).update(cable=None) # model.objects.filter(pk=instance.termination_id).update(cable=None)
@receiver(post_delete, sender=Cable) @receiver(post_delete, sender=Cable)
@ -129,15 +144,17 @@ def nullify_connected_endpoints(instance, **kwargs):
# model.objects.filter(pk__in=instance.termination_b_ids).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(_nodes__contains=instance):
cp = CablePath.from_origin(cablepath.origin) cablepath.delete()
if cp: # TODO: Create new traces
CablePath.objects.filter(pk=cablepath.pk).update( # cp = CablePath.from_origin(cablepath.origin)
_nodes=cp._nodes, # if cp:
destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None, # CablePath.objects.filter(pk=cablepath.pk).update(
destination_id=cp.destination.pk if cp.destination else None, # _nodes=cp._nodes,
is_active=cp.is_active, # destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
is_split=cp.is_split # destination_id=cp.destination.pk if cp.destination else None,
) # is_active=cp.is_active,
else: # is_split=cp.is_split
cablepath.delete() # )
# else:
# cablepath.delete()