Skip to content

Commit

Permalink
Closes #6071: Cable traces now traverse circuits
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Apr 1, 2021
1 parent d572223 commit 96759af
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 79 deletions.
2 changes: 2 additions & 0 deletions docs/release-notes/version-2.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ A new provider network model has been introduced to represent the boundary of a
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits

### REST API Changes

Expand All @@ -131,6 +132,7 @@ A new provider network model has been introduced to represent the boundary of a
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
* circuits.CircuitTermination
* Added the `provider_network` field
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
* circuits.ProviderNetwork
* Added the `/api/circuits/provider-networks/` endpoint
* dcim.Device
Expand Down
7 changes: 3 additions & 4 deletions netbox/circuits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Meta:
]


class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
provider_network = NestedProviderNetworkSerializer()
Expand All @@ -69,7 +69,6 @@ class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable',
]


Expand All @@ -91,7 +90,7 @@ class Meta:
]


class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False)
Expand All @@ -103,5 +102,5 @@ class Meta:
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied',
'_occupied',
]
3 changes: 1 addition & 2 deletions netbox/circuits/api/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.db.models import Prefetch
from rest_framework.routers import APIRootView

from circuits import filters
Expand Down Expand Up @@ -60,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet):

class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', '_path__destination', 'cable'
'circuit', 'site', 'provider_network', 'cable'
)
serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet
Expand Down
2 changes: 1 addition & 1 deletion netbox/circuits/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def search(self, queryset, name, value):
).distinct()


class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
Expand Down
32 changes: 32 additions & 0 deletions netbox/circuits/migrations/0029_circuit_tracing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.db import migrations
from django.db.models import Q


def delete_obsolete_cablepaths(apps, schema_editor):
"""
Delete all CablePath instances which originate or terminate at a CircuitTermination.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
CablePath = apps.get_model('dcim', 'CablePath')

ct = ContentType.objects.get_for_model(CircuitTermination)
CablePath.objects.filter(Q(origin_type=ct) | Q(destination_type=ct)).delete()


class Migration(migrations.Migration):

dependencies = [
('circuits', '0028_cache_circuit_terminations'),
]

operations = [
migrations.RemoveField(
model_name='circuittermination',
name='_path',
),
migrations.RunPython(
code=delete_obsolete_cablepaths,
reverse_code=migrations.RunPython.noop
),
]
2 changes: 1 addition & 1 deletion netbox/circuits/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def get_status_class(self):


@extras_features('webhooks')
class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
class CircuitTermination(ChangeLoggedModel, CableTermination):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,
Expand Down
6 changes: 0 additions & 6 deletions netbox/circuits/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,6 @@ def test_cabled(self):
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)


class ProviderNetworkTestCase(TestCase):
queryset = ProviderNetwork.objects.all()
Expand Down
4 changes: 0 additions & 4 deletions netbox/circuits/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,13 @@ def get_extra_context(self, request, instance):
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')

# Z-side termination
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')

return {
'termination_a': termination_a,
Expand Down
2 changes: 0 additions & 2 deletions netbox/dcim/management/commands/trace_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
from django.core.management.color import no_style
from django.db import connection

from circuits.models import CircuitTermination
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
from dcim.signals import create_cablepath

ENDPOINT_MODELS = (
CircuitTermination,
ConsolePort,
ConsoleServerPort,
Interface,
Expand Down
38 changes: 34 additions & 4 deletions netbox/dcim/models/cables.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ def from_origin(cls, origin):
"""
Create a new CablePath instance as traced from the given path origin.
"""
from circuits.models import CircuitTermination

if origin is None or origin.cable is None:
return None

Expand Down Expand Up @@ -441,6 +443,23 @@ def from_origin(cls, origin):
# No corresponding FrontPort found for the RearPort
break

# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
elif isinstance(peer_termination, CircuitTermination):
path.append(object_to_path_node(peer_termination))
# Get peer CircuitTermination
node = peer_termination.get_peer_termination()
if node:
path.append(object_to_path_node(node))
if node.provider_network:
destination = node.provider_network
break
elif node.site and not node.cable:
destination = node.site
break
else:
# No peer CircuitTermination exists; halt the trace
break

# Anything else marks the end of the path
else:
destination = peer_termination
Expand Down Expand Up @@ -486,15 +505,26 @@ def get_path(self):

return path

def get_cable_ids(self):
"""
Return all Cable IDs within the path.
"""
cable_ct = ContentType.objects.get_for_model(Cable).pk
cable_ids = []

for node in self.path:
ct, id = decompile_path_node(node)
if ct == cable_ct:
cable_ids.append(id)

return cable_ids

def get_total_length(self):
"""
Return a tuple containing the sum of the length of each cable in the path
and a flag indicating whether the length is definitive.
"""
cable_ids = [
# Starting from the first element, every third element in the path should be a Cable
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
]
cable_ids = self.get_cable_ids()
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
is_definitive = len(cables) == len(cable_ids)
Expand Down
9 changes: 5 additions & 4 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def parent_object(self):
class PathEndpoint(models.Model):
"""
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination.
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
Expand All @@ -184,10 +184,11 @@ def trace(self):

# Construct the complete path
path = [self, *self._path.get_path()]
while (len(path) + 1) % 3:
if self._path.destination:
path.append(self._path.destination)
while len(path) % 3:
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
path.append(None)
path.append(self._path.destination)
path.insert(-1, None)

# Return the path as a list of three-tuples (A termination, cable, B termination)
return list(zip(*[iter(path)] * 3))
Expand Down
6 changes: 4 additions & 2 deletions netbox/dcim/tables/template_code.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
CABLETERMINATION = """
{% if value %}
{% if value.parent_object %}
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
<i class="mdi mdi-chevron-right"></i>
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
{% endif %}
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
{% else %}
&mdash;
&mdash;
{% endif %}
"""

Expand Down
Loading

0 comments on commit 96759af

Please sign in to comment.