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.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -50,3 +52,12 @@ class MACAddressField(models.Field):
|
|||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
return str(self.to_python(value))
|
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__ = (
|
__all__ = (
|
||||||
'BaseInterface',
|
'BaseInterface',
|
||||||
'Cable',
|
'Cable',
|
||||||
|
'CablePath',
|
||||||
'CableTermination',
|
'CableTermination',
|
||||||
'ConsolePort',
|
'ConsolePort',
|
||||||
'ConsolePortTemplate',
|
'ConsolePortTemplate',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -32,6 +33,7 @@ __all__ = (
|
|||||||
'FrontPort',
|
'FrontPort',
|
||||||
'Interface',
|
'Interface',
|
||||||
'InventoryItem',
|
'InventoryItem',
|
||||||
|
'PathEndpoint',
|
||||||
'PowerOutlet',
|
'PowerOutlet',
|
||||||
'PowerPort',
|
'PowerPort',
|
||||||
'RearPort',
|
'RearPort',
|
||||||
@ -250,12 +252,23 @@ class CableTermination(models.Model):
|
|||||||
return endpoints
|
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
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('export_templates', 'webhooks')
|
@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.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -303,7 +316,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@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.
|
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')
|
@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.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -493,7 +506,7 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@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.
|
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')
|
@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.
|
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.choices import *
|
||||||
from dcim.constants 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.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
@ -25,6 +28,7 @@ from .device_components import *
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Cable',
|
'Cable',
|
||||||
|
'CablePath',
|
||||||
'Device',
|
'Device',
|
||||||
'DeviceRole',
|
'DeviceRole',
|
||||||
'DeviceType',
|
'DeviceType',
|
||||||
@ -1154,6 +1158,44 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
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
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
@ -1,10 +1,34 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
|
from django.db import transaction
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .choices import CableStatusChoices
|
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
||||||
from .models import Cable, CableTermination, Device, FrontPort, RearPort, 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)
|
@receiver(post_save, sender=VirtualChassis)
|
||||||
@ -32,7 +56,7 @@ def clear_virtualchassis_members(instance, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Cable)
|
@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
|
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
|
# Cache the Cable on its two termination points
|
||||||
if instance.termination_a.cable != instance:
|
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.cable = instance
|
||||||
instance.termination_a.save()
|
instance.termination_a.save()
|
||||||
if instance.termination_b.cable != instance:
|
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.cable = instance
|
||||||
instance.termination_b.save()
|
instance.termination_b.save()
|
||||||
|
|
||||||
# Update any endpoints for this Cable.
|
# Create/update cable paths
|
||||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
if created:
|
||||||
for endpoint in endpoints:
|
for termination in (instance.termination_a, instance.termination_b):
|
||||||
path, split_ends, position_stack = endpoint.trace()
|
if isinstance(termination, PathEndpoint):
|
||||||
# Determine overall path status (connected or planned)
|
create_cablepaths(termination)
|
||||||
path_status = True
|
else:
|
||||||
for segment in path:
|
rebuild_paths(termination)
|
||||||
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
|
else:
|
||||||
path_status = False
|
# We currently don't support modifying either termination of an existing Cable. This
|
||||||
break
|
# may change in the future.
|
||||||
|
pass
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Cable)
|
@receiver(pre_delete, sender=Cable)
|
||||||
@ -81,22 +92,28 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.dcim.cable')
|
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
|
# Disassociate the Cable from its termination points
|
||||||
if instance.termination_a is not None:
|
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.cable = None
|
||||||
instance.termination_a.save()
|
instance.termination_a.save()
|
||||||
if instance.termination_b is not None:
|
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.cable = None
|
||||||
instance.termination_b.save()
|
instance.termination_b.save()
|
||||||
|
|
||||||
# If this Cable was part of any complete end-to-end paths, tear them down.
|
# Delete any dependent cable paths
|
||||||
for endpoint in endpoints:
|
cable_paths = CablePath.objects.filter(path__contains=[object_to_path_node(instance)])
|
||||||
logger.debug(f"Removing path information for {endpoint}")
|
retrace_queue = [cp.origin for cp in cable_paths]
|
||||||
if hasattr(endpoint, 'connected_endpoint'):
|
deleted, _ = cable_paths.delete()
|
||||||
endpoint.connected_endpoint = None
|
logger.info(f'Deleted {deleted} cable paths')
|
||||||
endpoint.connection_status = None
|
|
||||||
endpoint.save()
|
# 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>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
{% elif iface.is_wireless %}
|
{% elif iface.is_wireless %}
|
||||||
<td colspan="2" class="text-muted">Wireless interface</td>
|
<td colspan="2" class="text-muted">Wireless interface</td>
|
||||||
{% elif iface.connected_endpoint.name %}
|
{% else %}
|
||||||
{# Connected to an Interface #}
|
{% with path_count=iface.get_connections.count %}
|
||||||
<td>
|
{% if path_count > 1 %}
|
||||||
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">
|
<td colspan="2">Multiple connections</td>
|
||||||
{{ iface.connected_endpoint.device }}
|
{% elif path_count %}
|
||||||
</a>
|
{% with endpoint=iface.get_connections.first.destination %}
|
||||||
</td>
|
<td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
|
||||||
<td>
|
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
|
||||||
<a href="{% url 'dcim:interface' pk=iface.connected_endpoint.pk %}">
|
{% endwith %}
|
||||||
<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 %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2" class="text-muted">Not connected</td>
|
||||||
<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 %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
|
||||||
<td colspan="2">
|
|
||||||
<span class="text-muted">Not connected</span>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Buttons #}
|
{# Buttons #}
|
||||||
|
Reference in New Issue
Block a user