|
@@ -12,6 +12,7 @@ from rest_framework.test import APIClient
|
|
|
from core.choices import ObjectChangeActionChoices
|
|
from core.choices import ObjectChangeActionChoices
|
|
|
from core.models import ObjectChange
|
|
from core.models import ObjectChange
|
|
|
from dcim.choices import CableProfileChoices, InterfaceTypeChoices
|
|
from dcim.choices import CableProfileChoices, InterfaceTypeChoices
|
|
|
|
|
+from dcim.exceptions import UnsupportedCablePath
|
|
|
from dcim.filtersets import InterfaceFilterSet
|
|
from dcim.filtersets import InterfaceFilterSet
|
|
|
from dcim.models import (
|
|
from dcim.models import (
|
|
|
Cable,
|
|
Cable,
|
|
@@ -29,6 +30,7 @@ from dcim.svg.cables import Connector
|
|
|
from dcim.tests.utils import BaseCablePathTestCase
|
|
from dcim.tests.utils import BaseCablePathTestCase
|
|
|
from users.constants import TOKEN_PREFIX
|
|
from users.constants import TOKEN_PREFIX
|
|
|
from users.models import Token, User
|
|
from users.models import Token, User
|
|
|
|
|
+from utilities.exceptions import AbortRequest
|
|
|
from utilities.ordering import naturalize_interface
|
|
from utilities.ordering import naturalize_interface
|
|
|
from utilities.testing import TestCase as ViewTestCase
|
|
from utilities.testing import TestCase as ViewTestCase
|
|
|
|
|
|
|
@@ -468,6 +470,88 @@ class ChannelizedCablePathTestCase(BaseCablePathTestCase):
|
|
|
self.assertIsNone(channel.cable_positions)
|
|
self.assertIsNone(channel.cable_positions)
|
|
|
self.assertPathIsNotSet(channel)
|
|
self.assertPathIsNotSet(channel)
|
|
|
|
|
|
|
|
|
|
+ def test_114_replacing_a_far_end_retires_the_channels_superseded_paths(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Replacing one far-end interface of a breakout cable must leave one path per channel in each direction. The
|
|
|
|
|
+ channels are the real origins, so the rows the retrace supersedes can only be found after the expansion.
|
|
|
|
|
+ """
|
|
|
|
|
+ parent, channels = self._create_channelized_interface('et0', 4)
|
|
|
|
|
+ far = [
|
|
|
|
|
+ Interface.objects.create(device=self.device, name=f'xe{i}', type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS)
|
|
|
|
|
+ for i in range(4)
|
|
|
|
|
+ ]
|
|
|
|
|
+ replacement = Interface.objects.create(
|
|
|
|
|
+ device=self.device, name='xe4', type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ cable = Cable(
|
|
|
|
|
+ profile=CableProfileChoices.BREAKOUT_1C4P_4C1P,
|
|
|
|
|
+ a_terminations=[parent],
|
|
|
|
|
+ b_terminations=far,
|
|
|
|
|
+ )
|
|
|
|
|
+ cable.clean()
|
|
|
|
|
+ cable.save()
|
|
|
|
|
+ self.assertEqual(CablePath.objects.count(), 8)
|
|
|
|
|
+
|
|
|
|
|
+ cable = Cable.objects.get(pk=cable.pk)
|
|
|
|
|
+ cable.b_terminations = [*far[:3], replacement]
|
|
|
|
|
+ cable.clean()
|
|
|
|
|
+ cable.save()
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual(CablePath.objects.count(), 8)
|
|
|
|
|
+ for channel, far_iface in zip(channels, [*far[:3], replacement]):
|
|
|
|
|
+ channel.refresh_from_db()
|
|
|
|
|
+ far_iface.refresh_from_db()
|
|
|
|
|
+ forward = self.assertPathExists((channel, cable, far_iface), is_complete=True, is_active=True)
|
|
|
|
|
+ reverse = self.assertPathExists((far_iface, cable, channel), is_complete=True, is_active=True)
|
|
|
|
|
+ self.assertPathIsSet(channel, forward)
|
|
|
|
|
+ self.assertPathIsSet(far_iface, reverse)
|
|
|
|
|
+ far[3].refresh_from_db()
|
|
|
|
|
+ self.assertIsNone(far[3].cable)
|
|
|
|
|
+ self.assertPathIsNotSet(far[3])
|
|
|
|
|
+
|
|
|
|
|
+ def test_115_failed_channel_trace_restores_the_replaced_paths(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ A trace that raises partway through a channelized end must leave every channel's stored path in place.
|
|
|
|
|
+ """
|
|
|
|
|
+ parent, channels = self._create_channelized_interface('et0', 2)
|
|
|
|
|
+ far = [
|
|
|
|
|
+ Interface.objects.create(device=self.device, name=f'xe{i}', type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS)
|
|
|
|
|
+ for i in range(2)
|
|
|
|
|
+ ]
|
|
|
|
|
+ cable = Cable(
|
|
|
|
|
+ profile=CableProfileChoices.BREAKOUT_1C2P_2C1P,
|
|
|
|
|
+ a_terminations=[parent],
|
|
|
|
|
+ b_terminations=far,
|
|
|
|
|
+ )
|
|
|
|
|
+ cable.clean()
|
|
|
|
|
+ cable.save()
|
|
|
|
|
+ self.assertEqual(CablePath.objects.count(), 4)
|
|
|
|
|
+
|
|
|
|
|
+ stored_paths = {}
|
|
|
|
|
+ for channel in channels:
|
|
|
|
|
+ channel.refresh_from_db()
|
|
|
|
|
+ stored_paths[channel.pk] = channel._path
|
|
|
|
|
+
|
|
|
|
|
+ traced = CablePath.from_origin
|
|
|
|
|
+
|
|
|
|
|
+ def failing_from_origin(terminations):
|
|
|
|
|
+ if terminations and terminations[0] == channels[1]:
|
|
|
|
|
+ raise UnsupportedCablePath('Simulated trace error')
|
|
|
|
|
+ return traced(terminations)
|
|
|
|
|
+
|
|
|
|
|
+ cable = Cable.objects.get(pk=cable.pk)
|
|
|
|
|
+ cable.b_terminations = far
|
|
|
|
|
+ with mock.patch.object(CablePath, 'from_origin', side_effect=failing_from_origin):
|
|
|
|
|
+ with self.assertRaises(AbortRequest):
|
|
|
|
|
+ cable.save()
|
|
|
|
|
+
|
|
|
|
|
+ # Reaching a query at all proves the block kept its savepoint
|
|
|
|
|
+ self.assertEqual(CablePath.objects.count(), 4)
|
|
|
|
|
+ for channel in channels:
|
|
|
|
|
+ channel.refresh_from_db()
|
|
|
|
|
+ self.assertPathIsSet(channel, stored_paths[channel.pk])
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class ChannelizedInterfaceTestCase(TestCase):
|
|
class ChannelizedInterfaceTestCase(TestCase):
|
|
|
"""
|
|
"""
|