mirror of
				https://github.com/netbox-community/netbox.git
				synced 2024-05-10 07:54:54 +00:00 
			
		
		
		
	Initial work on cable paths (WIP)
This commit is contained in:
		@@ -1,3 +1,5 @@
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.contrib.postgres.validators import ArrayMaxLengthValidator
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.core.validators import MinValueValidator, MaxValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
@@ -50,3 +52,12 @@ class MACAddressField(models.Field):
 | 
			
		||||
        if not value:
 | 
			
		||||
            return None
 | 
			
		||||
        return str(self.to_python(value))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PathField(ArrayField):
 | 
			
		||||
    """
 | 
			
		||||
    An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        kwargs['base_field'] = models.CharField(max_length=40)
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								netbox/dcim/managers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								netbox/dcim/managers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db.models import Manager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CablePathManager(Manager):
 | 
			
		||||
 | 
			
		||||
    def create_for_endpoint(self, endpoint):
 | 
			
		||||
        ct = ContentType.objects.get_for_model(endpoint)
 | 
			
		||||
							
								
								
									
										27
									
								
								netbox/dcim/migrations/0120_cablepath.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								netbox/dcim/migrations/0120_cablepath.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.1 on 2020-09-30 18:09
 | 
			
		||||
 | 
			
		||||
import dcim.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('contenttypes', '0002_remove_content_type_name'),
 | 
			
		||||
        ('dcim', '0119_inventoryitem_mptt_rebuild'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CablePath',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
 | 
			
		||||
                ('origin_id', models.PositiveIntegerField()),
 | 
			
		||||
                ('destination_id', models.PositiveIntegerField(blank=True, null=True)),
 | 
			
		||||
                ('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)),
 | 
			
		||||
                ('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
 | 
			
		||||
                ('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -8,6 +8,7 @@ from .sites import *
 | 
			
		||||
__all__ = (
 | 
			
		||||
    'BaseInterface',
 | 
			
		||||
    'Cable',
 | 
			
		||||
    'CablePath',
 | 
			
		||||
    'CableTermination',
 | 
			
		||||
    'ConsolePort',
 | 
			
		||||
    'ConsolePortTemplate',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.contrib.contenttypes.fields import GenericRelation
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
@@ -32,6 +33,7 @@ __all__ = (
 | 
			
		||||
    'FrontPort',
 | 
			
		||||
    'Interface',
 | 
			
		||||
    'InventoryItem',
 | 
			
		||||
    'PathEndpoint',
 | 
			
		||||
    'PowerOutlet',
 | 
			
		||||
    'PowerPort',
 | 
			
		||||
    'RearPort',
 | 
			
		||||
@@ -250,12 +252,23 @@ class CableTermination(models.Model):
 | 
			
		||||
        return endpoints
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PathEndpoint:
 | 
			
		||||
 | 
			
		||||
    def get_connections(self):
 | 
			
		||||
        from dcim.models import CablePath
 | 
			
		||||
        return CablePath.objects.filter(
 | 
			
		||||
            origin_type=ContentType.objects.get_for_model(self),
 | 
			
		||||
            origin_id=self.pk,
 | 
			
		||||
            destination_id__isnull=False
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Console ports
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
@extras_features('export_templates', 'webhooks')
 | 
			
		||||
class ConsolePort(CableTermination, ComponentModel):
 | 
			
		||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
 | 
			
		||||
    """
 | 
			
		||||
    A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
 | 
			
		||||
    """
 | 
			
		||||
@@ -303,7 +316,7 @@ class ConsolePort(CableTermination, ComponentModel):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
@extras_features('webhooks')
 | 
			
		||||
class ConsoleServerPort(CableTermination, ComponentModel):
 | 
			
		||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
 | 
			
		||||
    """
 | 
			
		||||
    A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
 | 
			
		||||
    """
 | 
			
		||||
@@ -344,7 +357,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
@extras_features('export_templates', 'webhooks')
 | 
			
		||||
class PowerPort(CableTermination, ComponentModel):
 | 
			
		||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
 | 
			
		||||
    """
 | 
			
		||||
    A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
 | 
			
		||||
    """
 | 
			
		||||
@@ -493,7 +506,7 @@ class PowerPort(CableTermination, ComponentModel):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
@extras_features('webhooks')
 | 
			
		||||
class PowerOutlet(CableTermination, ComponentModel):
 | 
			
		||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
 | 
			
		||||
    """
 | 
			
		||||
    A physical power outlet (output) within a Device which provides power to a PowerPort.
 | 
			
		||||
    """
 | 
			
		||||
@@ -585,7 +598,7 @@ class BaseInterface(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@extras_features('export_templates', 'webhooks')
 | 
			
		||||
class Interface(CableTermination, ComponentModel, BaseInterface):
 | 
			
		||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
 | 
			
		||||
    """
 | 
			
		||||
    A network interface within a Device. A physical Interface can connect to exactly one other Interface.
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,9 @@ from taggit.managers import TaggableManager
 | 
			
		||||
 | 
			
		||||
from dcim.choices import *
 | 
			
		||||
from dcim.constants import *
 | 
			
		||||
from dcim.fields import PathField
 | 
			
		||||
from dcim.managers import CablePathManager
 | 
			
		||||
from dcim.utils import path_node_to_object
 | 
			
		||||
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
 | 
			
		||||
from extras.utils import extras_features
 | 
			
		||||
from utilities.choices import ColorChoices
 | 
			
		||||
@@ -25,6 +28,7 @@ from .device_components import *
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    'Cable',
 | 
			
		||||
    'CablePath',
 | 
			
		||||
    'Device',
 | 
			
		||||
    'DeviceRole',
 | 
			
		||||
    'DeviceType',
 | 
			
		||||
@@ -1154,6 +1158,44 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
 | 
			
		||||
        return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CablePath(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    An array of objects conveying the end-to-end path of one or more Cables.
 | 
			
		||||
    """
 | 
			
		||||
    origin_type = models.ForeignKey(
 | 
			
		||||
        to=ContentType,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='+'
 | 
			
		||||
    )
 | 
			
		||||
    origin_id = models.PositiveIntegerField()
 | 
			
		||||
    origin = GenericForeignKey(
 | 
			
		||||
        ct_field='origin_type',
 | 
			
		||||
        fk_field='origin_id'
 | 
			
		||||
    )
 | 
			
		||||
    destination_type = models.ForeignKey(
 | 
			
		||||
        to=ContentType,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='+',
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True
 | 
			
		||||
    )
 | 
			
		||||
    destination_id = models.PositiveIntegerField(
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True
 | 
			
		||||
    )
 | 
			
		||||
    destination = GenericForeignKey(
 | 
			
		||||
        ct_field='destination_type',
 | 
			
		||||
        fk_field='destination_id'
 | 
			
		||||
    )
 | 
			
		||||
    path = PathField()
 | 
			
		||||
 | 
			
		||||
    objects = CablePathManager()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        path = ', '.join([str(path_node_to_object(node)) for node in self.path])
 | 
			
		||||
        return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Virtual chassis
 | 
			
		||||
#
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,34 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db.models.signals import post_save, pre_delete
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
 | 
			
		||||
from .choices import CableStatusChoices
 | 
			
		||||
from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis
 | 
			
		||||
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
 | 
			
		||||
from .utils import object_to_path_node, trace_paths
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_cablepaths(node):
 | 
			
		||||
    """
 | 
			
		||||
    Create CablePaths for all paths originating from the specified node.
 | 
			
		||||
    """
 | 
			
		||||
    for path, destination in trace_paths(node):
 | 
			
		||||
        cp = CablePath(origin=node, path=path, destination=destination)
 | 
			
		||||
        cp.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def rebuild_paths(obj):
 | 
			
		||||
    """
 | 
			
		||||
    Rebuild all CablePaths which traverse the specified node
 | 
			
		||||
    """
 | 
			
		||||
    node = object_to_path_node(obj)
 | 
			
		||||
    cable_paths = CablePath.objects.filter(path__contains=[node])
 | 
			
		||||
 | 
			
		||||
    with transaction.atomic():
 | 
			
		||||
        for cp in cable_paths:
 | 
			
		||||
            cp.delete()
 | 
			
		||||
            create_cablepaths(cp.origin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=VirtualChassis)
 | 
			
		||||
@@ -32,7 +56,7 @@ def clear_virtualchassis_members(instance, **kwargs):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=Cable)
 | 
			
		||||
def update_connected_endpoints(instance, **kwargs):
 | 
			
		||||
def update_connected_endpoints(instance, created, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    When a Cable is saved, check for and update its two connected endpoints
 | 
			
		||||
    """
 | 
			
		||||
@@ -40,38 +64,25 @@ def update_connected_endpoints(instance, **kwargs):
 | 
			
		||||
 | 
			
		||||
    # Cache the Cable on its two termination points
 | 
			
		||||
    if instance.termination_a.cable != instance:
 | 
			
		||||
        logger.debug("Updating termination A for cable {}".format(instance))
 | 
			
		||||
        logger.debug(f"Updating termination A for cable {instance}")
 | 
			
		||||
        instance.termination_a.cable = instance
 | 
			
		||||
        instance.termination_a.save()
 | 
			
		||||
    if instance.termination_b.cable != instance:
 | 
			
		||||
        logger.debug("Updating termination B for cable {}".format(instance))
 | 
			
		||||
        logger.debug(f"Updating termination B for cable {instance}")
 | 
			
		||||
        instance.termination_b.cable = instance
 | 
			
		||||
        instance.termination_b.save()
 | 
			
		||||
 | 
			
		||||
    # Update any endpoints for this Cable.
 | 
			
		||||
    endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
 | 
			
		||||
    for endpoint in endpoints:
 | 
			
		||||
        path, split_ends, position_stack = endpoint.trace()
 | 
			
		||||
        # Determine overall path status (connected or planned)
 | 
			
		||||
        path_status = True
 | 
			
		||||
        for segment in path:
 | 
			
		||||
            if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
 | 
			
		||||
                path_status = False
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        endpoint_a = path[0][0]
 | 
			
		||||
        endpoint_b = path[-1][2] if not split_ends and not position_stack else None
 | 
			
		||||
 | 
			
		||||
        # Patch panel ports are not connected endpoints, all other cable terminations are
 | 
			
		||||
        if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \
 | 
			
		||||
                isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)):
 | 
			
		||||
            logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
 | 
			
		||||
            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()
 | 
			
		||||
    # Create/update cable paths
 | 
			
		||||
    if created:
 | 
			
		||||
        for termination in (instance.termination_a, instance.termination_b):
 | 
			
		||||
            if isinstance(termination, PathEndpoint):
 | 
			
		||||
                create_cablepaths(termination)
 | 
			
		||||
            else:
 | 
			
		||||
                rebuild_paths(termination)
 | 
			
		||||
    else:
 | 
			
		||||
        # We currently don't support modifying either termination of an existing Cable. This
 | 
			
		||||
        # may change in the future.
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(pre_delete, sender=Cable)
 | 
			
		||||
@@ -81,22 +92,28 @@ def nullify_connected_endpoints(instance, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    logger = logging.getLogger('netbox.dcim.cable')
 | 
			
		||||
 | 
			
		||||
    endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
 | 
			
		||||
 | 
			
		||||
    # Disassociate the Cable from its termination points
 | 
			
		||||
    if instance.termination_a is not None:
 | 
			
		||||
        logger.debug("Nullifying termination A for cable {}".format(instance))
 | 
			
		||||
        logger.debug(f"Nullifying termination A for cable {instance}")
 | 
			
		||||
        instance.termination_a.cable = None
 | 
			
		||||
        instance.termination_a.save()
 | 
			
		||||
    if instance.termination_b is not None:
 | 
			
		||||
        logger.debug("Nullifying termination B for cable {}".format(instance))
 | 
			
		||||
        logger.debug(f"Nullifying termination B for cable {instance}")
 | 
			
		||||
        instance.termination_b.cable = None
 | 
			
		||||
        instance.termination_b.save()
 | 
			
		||||
 | 
			
		||||
    # If this Cable was part of any complete end-to-end paths, tear them down.
 | 
			
		||||
    for endpoint in endpoints:
 | 
			
		||||
        logger.debug(f"Removing path information for {endpoint}")
 | 
			
		||||
        if hasattr(endpoint, 'connected_endpoint'):
 | 
			
		||||
            endpoint.connected_endpoint = None
 | 
			
		||||
            endpoint.connection_status = None
 | 
			
		||||
            endpoint.save()
 | 
			
		||||
    # Delete any dependent cable paths
 | 
			
		||||
    cable_paths = CablePath.objects.filter(path__contains=[object_to_path_node(instance)])
 | 
			
		||||
    retrace_queue = [cp.origin for cp in cable_paths]
 | 
			
		||||
    deleted, _ = cable_paths.delete()
 | 
			
		||||
    logger.info(f'Deleted {deleted} cable paths')
 | 
			
		||||
 | 
			
		||||
    # Retrace cable paths from the origins of deleted paths
 | 
			
		||||
    for origin in retrace_queue:
 | 
			
		||||
        # Delete and recreate all CablePaths for this origin point
 | 
			
		||||
        # TODO: We can probably be smarter about skipping unchanged paths
 | 
			
		||||
        CablePath.objects.filter(
 | 
			
		||||
            origin_type=ContentType.objects.get_for_model(origin),
 | 
			
		||||
            origin_id=origin.pk
 | 
			
		||||
        ).delete()
 | 
			
		||||
        create_cablepaths(origin)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										65
									
								
								netbox/dcim/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								netbox/dcim/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
 | 
			
		||||
from .models import FrontPort, RearPort
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def object_to_path_node(obj):
 | 
			
		||||
    return f'{obj._meta.model_name}:{obj.pk}'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def objects_to_path(*obj_list):
 | 
			
		||||
    return [object_to_path_node(obj) for obj in obj_list]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def path_node_to_object(repr):
 | 
			
		||||
    model_name, object_id = repr.split(':')
 | 
			
		||||
    model_class = ContentType.objects.get(model=model_name).model_class()
 | 
			
		||||
    return model_class.objects.get(pk=int(object_id))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def trace_paths(node):
 | 
			
		||||
    destination = None
 | 
			
		||||
    path = []
 | 
			
		||||
    position_stack = []
 | 
			
		||||
 | 
			
		||||
    if node.cable is None:
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
    while node.cable is not None:
 | 
			
		||||
 | 
			
		||||
        # Follow the cable to its far-end termination
 | 
			
		||||
        path.append(object_to_path_node(node.cable))
 | 
			
		||||
        peer_termination = node.get_cable_peer()
 | 
			
		||||
 | 
			
		||||
        # Follow a FrontPort to its corresponding RearPort
 | 
			
		||||
        if isinstance(peer_termination, FrontPort):
 | 
			
		||||
            path.append(object_to_path_node(peer_termination))
 | 
			
		||||
            position_stack.append(peer_termination.rear_port_position)
 | 
			
		||||
            node = peer_termination.rear_port
 | 
			
		||||
            path.append(object_to_path_node(node))
 | 
			
		||||
 | 
			
		||||
        # Follow a RearPort to its corresponding FrontPort
 | 
			
		||||
        elif isinstance(peer_termination, RearPort):
 | 
			
		||||
            path.append(object_to_path_node(peer_termination))
 | 
			
		||||
            if position_stack:
 | 
			
		||||
                position = position_stack.pop()
 | 
			
		||||
                node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
 | 
			
		||||
                path.append(object_to_path_node(node))
 | 
			
		||||
            else:
 | 
			
		||||
                # No position indicated, so we have to trace _all_ peer FrontPorts
 | 
			
		||||
                paths = []
 | 
			
		||||
                for frontport in FrontPort.objects.filter(rear_port=peer_termination):
 | 
			
		||||
                    branches = trace_paths(frontport)
 | 
			
		||||
                    if branches:
 | 
			
		||||
                        for branch, destination in branches:
 | 
			
		||||
                            paths.append(([*path, object_to_path_node(frontport), *branch], destination))
 | 
			
		||||
                    else:
 | 
			
		||||
                        paths.append(([*path, object_to_path_node(frontport)], None))
 | 
			
		||||
                return paths
 | 
			
		||||
 | 
			
		||||
        # Anything else marks the end of the path
 | 
			
		||||
        else:
 | 
			
		||||
            destination = peer_termination
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    return [(path, destination)]
 | 
			
		||||
@@ -75,65 +75,19 @@
 | 
			
		||||
        <td colspan="2" class="text-muted">Virtual interface</td>
 | 
			
		||||
    {% elif iface.is_wireless %}
 | 
			
		||||
        <td colspan="2" class="text-muted">Wireless interface</td>
 | 
			
		||||
    {% elif iface.connected_endpoint.name %}
 | 
			
		||||
        {# Connected to an Interface #}
 | 
			
		||||
        <td>
 | 
			
		||||
            <a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">
 | 
			
		||||
                {{ iface.connected_endpoint.device }}
 | 
			
		||||
            </a>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>
 | 
			
		||||
            <a href="{% url 'dcim:interface' pk=iface.connected_endpoint.pk %}">
 | 
			
		||||
                <span title="{{ iface.connected_endpoint.get_type_display }}">
 | 
			
		||||
                    {{ iface.connected_endpoint }}
 | 
			
		||||
                </span>
 | 
			
		||||
            </a>
 | 
			
		||||
        </td>
 | 
			
		||||
    {% elif iface.connected_endpoint.term_side %}
 | 
			
		||||
        {# Connected to a CircuitTermination #}
 | 
			
		||||
        {% with iface.connected_endpoint.get_peer_termination as peer_termination %}
 | 
			
		||||
            {% if peer_termination %}
 | 
			
		||||
                {% if peer_termination.connected_endpoint %}
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">
 | 
			
		||||
                            {{ peer_termination.connected_endpoint.device }}
 | 
			
		||||
                        </a><br/>
 | 
			
		||||
                        <small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
 | 
			
		||||
                            <a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
 | 
			
		||||
                                {{ iface.connected_endpoint.circuit.provider }}
 | 
			
		||||
                                {{ iface.connected_endpoint.circuit }}
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </small>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {{ peer_termination.connected_endpoint }}
 | 
			
		||||
                    </td>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    <td colspan="2">
 | 
			
		||||
                        <a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">
 | 
			
		||||
                            {{ peer_termination.site }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        via <i class="fa fa-fw fa-globe" title="Circuit"></i>
 | 
			
		||||
                        <a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
 | 
			
		||||
                            {{ iface.connected_endpoint.circuit.provider }}
 | 
			
		||||
                            {{ iface.connected_endpoint.circuit }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
    {% else %}
 | 
			
		||||
        {% with path_count=iface.get_connections.count %}
 | 
			
		||||
            {% if path_count > 1 %}
 | 
			
		||||
                <td colspan="2">Multiple connections</td>
 | 
			
		||||
            {% elif path_count %}
 | 
			
		||||
                {% with endpoint=iface.get_connections.first.destination %}
 | 
			
		||||
                    <td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
 | 
			
		||||
                    <td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
 | 
			
		||||
                {% endwith %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
                <td colspan="2">
 | 
			
		||||
                    <i class="fa fa-fw fa-globe" title="Circuit"></i>
 | 
			
		||||
                    <a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
 | 
			
		||||
                        {{ iface.connected_endpoint.circuit.provider }}
 | 
			
		||||
                        {{ iface.connected_endpoint.circuit }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td colspan="2" class="text-muted">Not connected</td>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
    {% else %}
 | 
			
		||||
        <td colspan="2">
 | 
			
		||||
            <span class="text-muted">Not connected</span>
 | 
			
		||||
        </td>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {# Buttons #}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user