diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py new file mode 100644 index 0000000000..e4be1b5f1f --- /dev/null +++ b/netbox/dcim/exceptions.py @@ -0,0 +1,2 @@ +class UnsupportedCablePath(Exception): + pass diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 2fac55dd40..e94e72fd31 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -27,6 +27,7 @@ 'CableTermination', ) +from ..exceptions import UnsupportedCablePath trace_paths = Signal() @@ -533,8 +534,8 @@ def from_origin(cls, terminations): return None # Ensure all originating terminations are attached to the same link - if len(terminations) > 1: - assert all(t.link == terminations[0].link for t in terminations[1:]) + if len(terminations) > 1 and not all(t.link == terminations[0].link for t in terminations[1:]): + raise UnsupportedCablePath(_("All originating terminations must start must be attached to the same link")) path = [] position_stack = [] @@ -545,12 +546,13 @@ def from_origin(cls, terminations): while terminations: # Terminations must all be of the same type - assert all(isinstance(t, type(terminations[0])) for t in terminations[1:]) + if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]): + raise UnsupportedCablePath(_("All mid-span terminations must have the same termination type")) # All mid-span terminations must all be attached to the same device - if not isinstance(terminations[0], PathEndpoint): - assert all(isinstance(t, type(terminations[0])) for t in terminations[1:]) - assert all(t.parent_object == terminations[0].parent_object for t in terminations[1:]) + if (not isinstance(terminations[0], PathEndpoint) and not + all(t.parent_object == terminations[0].parent_object for t in terminations[1:])): + raise UnsupportedCablePath(_("All mid-span terminations must have the same parent object")) # Check for a split path (e.g. rear port fanning out to multiple front ports with # different cables attached) @@ -573,8 +575,10 @@ def from_origin(cls, terminations): return None # Otherwise, halt the trace if no link exists break - assert all(type(link) in (Cable, WirelessLink) for link in links) - assert all(isinstance(link, type(links[0])) for link in links) + if not all(type(link) in (Cable, WirelessLink) for link in links): + raise UnsupportedCablePath(_("All links must be cable or wireless")) + if not all(isinstance(link, type(links[0])) for link in links): + raise UnsupportedCablePath(_("All links must match first link type")) # Step 3: Record asymmetric paths as split not_connected_terminations = [termination.link for termination in terminations if termination.link is None] @@ -651,14 +655,18 @@ def from_origin(cls, terminations): positions = position_stack.pop() # Ensure we have a number of positions equal to the amount of remote terminations - assert len(remote_terminations) == len(positions) + if len(remote_terminations) != len(positions): + raise UnsupportedCablePath( + _("All positions counts within the path on opposite ends of links must match") + ) # Get our front ports q_filter = Q() for rt in remote_terminations: position = positions.pop() q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position) - assert q_filter is not Q() + if q_filter is Q(): + raise UnsupportedCablePath(_("Remote termination position filter is missing")) front_ports = FrontPort.objects.filter(q_filter) # Obtain the individual front ports based on the termination and position elif position_stack: diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index cd7b0e6d79..b6161af915 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,6 +2,7 @@ from circuits.models import * from dcim.choices import LinkStatusChoices +from dcim.exceptions import UnsupportedCablePath from dcim.models import * from dcim.svg import CableTraceSVG from dcim.utils import object_to_path_node @@ -2261,7 +2262,7 @@ def test_401_exclude_midspan_devices(self): b_terminations=[frontport1, frontport3], label='C1' ) - with self.assertRaises(AssertionError): + with self.assertRaises(UnsupportedCablePath): cable1.save() self.assertPathDoesNotExist( @@ -2280,7 +2281,7 @@ def test_401_exclude_midspan_devices(self): label='C3' ) - with self.assertRaises(AssertionError): + with self.assertRaises(UnsupportedCablePath): cable3.save() self.assertPathDoesNotExist( diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 3572645220..6212ba7f4e 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -13,6 +13,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ +from dcim.exceptions import UnsupportedCablePath from core.signals import clear_events from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation @@ -319,6 +320,12 @@ def post(self, request, *args, **kwargs): form.add_error(None, e.message) clear_events.send(sender=self) + # Catch any validation errors thrown in the model.save() or form.save() methods + except UnsupportedCablePath as e: + logger.debug(e.message) + form.add_error(None, e.message) + clear_events.send(sender=self) + else: logger.debug("Form validation failed")