cables.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. import itertools
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.core.exceptions import ValidationError
  4. from django.db import models
  5. from django.db.models import Sum
  6. from django.dispatch import Signal
  7. from django.utils.translation import gettext_lazy as _
  8. from core.models import ObjectType
  9. from dcim.choices import *
  10. from dcim.constants import *
  11. from dcim.fields import PathField
  12. from dcim.utils import decompile_path_node, object_to_path_node
  13. from netbox.models import ChangeLoggedModel, PrimaryModel
  14. from utilities.conversion import to_meters
  15. from utilities.fields import ColorField, GenericArrayForeignKey
  16. from utilities.querysets import RestrictedQuerySet
  17. from wireless.models import WirelessLink
  18. from .device_components import FrontPort, RearPort, PathEndpoint
  19. __all__ = (
  20. 'Cable',
  21. 'CablePath',
  22. 'CableTermination',
  23. )
  24. trace_paths = Signal()
  25. #
  26. # Cables
  27. #
  28. class Cable(PrimaryModel):
  29. """
  30. A physical connection between two endpoints.
  31. """
  32. type = models.CharField(
  33. verbose_name=_('type'),
  34. max_length=50,
  35. choices=CableTypeChoices,
  36. blank=True,
  37. null=True
  38. )
  39. status = models.CharField(
  40. verbose_name=_('status'),
  41. max_length=50,
  42. choices=LinkStatusChoices,
  43. default=LinkStatusChoices.STATUS_CONNECTED
  44. )
  45. tenant = models.ForeignKey(
  46. to='tenancy.Tenant',
  47. on_delete=models.PROTECT,
  48. related_name='cables',
  49. blank=True,
  50. null=True
  51. )
  52. label = models.CharField(
  53. verbose_name=_('label'),
  54. max_length=100,
  55. blank=True
  56. )
  57. color = ColorField(
  58. verbose_name=_('color'),
  59. blank=True
  60. )
  61. length = models.DecimalField(
  62. verbose_name=_('length'),
  63. max_digits=8,
  64. decimal_places=2,
  65. blank=True,
  66. null=True
  67. )
  68. length_unit = models.CharField(
  69. verbose_name=_('length unit'),
  70. max_length=50,
  71. choices=CableLengthUnitChoices,
  72. blank=True,
  73. null=True
  74. )
  75. # Stores the normalized length (in meters) for database ordering
  76. _abs_length = models.DecimalField(
  77. max_digits=10,
  78. decimal_places=4,
  79. blank=True,
  80. null=True
  81. )
  82. clone_fields = ('tenant', 'type',)
  83. class Meta:
  84. ordering = ('pk',)
  85. verbose_name = _('cable')
  86. verbose_name_plural = _('cables')
  87. def __init__(self, *args, a_terminations=None, b_terminations=None, **kwargs):
  88. super().__init__(*args, **kwargs)
  89. # A copy of the PK to be used by __str__ in case the object is deleted
  90. self._pk = self.__dict__.get('id')
  91. # Cache the original status so we can check later if it's been changed
  92. self._orig_status = self.__dict__.get('status')
  93. self._terminations_modified = False
  94. # Assign or retrieve A/B terminations
  95. if a_terminations:
  96. self.a_terminations = a_terminations
  97. if b_terminations:
  98. self.b_terminations = b_terminations
  99. def __str__(self):
  100. pk = self.pk or self._pk
  101. return self.label or f'#{pk}'
  102. @property
  103. def a_terminations(self):
  104. if hasattr(self, '_a_terminations'):
  105. return self._a_terminations
  106. if not self.pk:
  107. return []
  108. # Query self.terminations.all() to leverage cached results
  109. return [
  110. ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A
  111. ]
  112. @a_terminations.setter
  113. def a_terminations(self, value):
  114. if not self.pk or self.a_terminations != list(value):
  115. self._terminations_modified = True
  116. self._a_terminations = value
  117. @property
  118. def b_terminations(self):
  119. if hasattr(self, '_b_terminations'):
  120. return self._b_terminations
  121. if not self.pk:
  122. return []
  123. # Query self.terminations.all() to leverage cached results
  124. return [
  125. ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B
  126. ]
  127. @b_terminations.setter
  128. def b_terminations(self, value):
  129. if not self.pk or self.b_terminations != list(value):
  130. self._terminations_modified = True
  131. self._b_terminations = value
  132. def clean(self):
  133. super().clean()
  134. # Validate length and length_unit
  135. if self.length is not None and not self.length_unit:
  136. raise ValidationError(_("Must specify a unit when setting a cable length"))
  137. if self._state.adding and self.pk is None and (not self.a_terminations or not self.b_terminations):
  138. raise ValidationError(_("Must define A and B terminations when creating a new cable."))
  139. if self._terminations_modified:
  140. # Check that all termination objects for either end are of the same type
  141. for terms in (self.a_terminations, self.b_terminations):
  142. if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]):
  143. raise ValidationError(_("Cannot connect different termination types to same end of cable."))
  144. # Check that termination types are compatible
  145. if self.a_terminations and self.b_terminations:
  146. a_type = self.a_terminations[0]._meta.model_name
  147. b_type = self.b_terminations[0]._meta.model_name
  148. if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
  149. raise ValidationError(
  150. _("Incompatible termination types: {type_a} and {type_b}").format(type_a=a_type, type_b=b_type)
  151. )
  152. if a_type == b_type:
  153. # can't directly use self.a_terminations here as possible they
  154. # don't have pk yet
  155. a_pks = set(obj.pk for obj in self.a_terminations if obj.pk)
  156. b_pks = set(obj.pk for obj in self.b_terminations if obj.pk)
  157. if (a_pks & b_pks):
  158. raise ValidationError(
  159. _("A and B terminations cannot connect to the same object.")
  160. )
  161. # Run clean() on any new CableTerminations
  162. for termination in self.a_terminations:
  163. CableTermination(cable=self, cable_end='A', termination=termination).clean()
  164. for termination in self.b_terminations:
  165. CableTermination(cable=self, cable_end='B', termination=termination).clean()
  166. def save(self, *args, **kwargs):
  167. _created = self.pk is None
  168. # Store the given length (if any) in meters for use in database ordering
  169. if self.length is not None and self.length_unit:
  170. self._abs_length = to_meters(self.length, self.length_unit)
  171. else:
  172. self._abs_length = None
  173. # Clear length_unit if no length is defined
  174. if self.length is None:
  175. self.length_unit = None
  176. super().save(*args, **kwargs)
  177. # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
  178. self._pk = self.pk
  179. # Retrieve existing A/B terminations for the Cable
  180. a_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='A')}
  181. b_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='B')}
  182. # Delete stale CableTerminations
  183. if self._terminations_modified:
  184. for termination, ct in a_terminations.items():
  185. if termination.pk and termination not in self.a_terminations:
  186. ct.delete()
  187. for termination, ct in b_terminations.items():
  188. if termination.pk and termination not in self.b_terminations:
  189. ct.delete()
  190. # Save new CableTerminations (if any)
  191. if self._terminations_modified:
  192. for termination in self.a_terminations:
  193. if not termination.pk or termination not in a_terminations:
  194. CableTermination(cable=self, cable_end='A', termination=termination).save()
  195. for termination in self.b_terminations:
  196. if not termination.pk or termination not in b_terminations:
  197. CableTermination(cable=self, cable_end='B', termination=termination).save()
  198. trace_paths.send(Cable, instance=self, created=_created)
  199. def get_status_color(self):
  200. return LinkStatusChoices.colors.get(self.status)
  201. class CableTermination(ChangeLoggedModel):
  202. """
  203. A mapping between side A or B of a Cable and a terminating object (e.g. an Interface or CircuitTermination).
  204. """
  205. cable = models.ForeignKey(
  206. to='dcim.Cable',
  207. on_delete=models.CASCADE,
  208. related_name='terminations'
  209. )
  210. cable_end = models.CharField(
  211. max_length=1,
  212. choices=CableEndChoices,
  213. verbose_name=_('end')
  214. )
  215. termination_type = models.ForeignKey(
  216. to='contenttypes.ContentType',
  217. limit_choices_to=CABLE_TERMINATION_MODELS,
  218. on_delete=models.PROTECT,
  219. related_name='+'
  220. )
  221. termination_id = models.PositiveBigIntegerField()
  222. termination = GenericForeignKey(
  223. ct_field='termination_type',
  224. fk_field='termination_id'
  225. )
  226. # Cached associations to enable efficient filtering
  227. _device = models.ForeignKey(
  228. to='dcim.Device',
  229. on_delete=models.CASCADE,
  230. blank=True,
  231. null=True
  232. )
  233. _rack = models.ForeignKey(
  234. to='dcim.Rack',
  235. on_delete=models.CASCADE,
  236. blank=True,
  237. null=True
  238. )
  239. _location = models.ForeignKey(
  240. to='dcim.Location',
  241. on_delete=models.CASCADE,
  242. blank=True,
  243. null=True
  244. )
  245. _site = models.ForeignKey(
  246. to='dcim.Site',
  247. on_delete=models.CASCADE,
  248. blank=True,
  249. null=True
  250. )
  251. objects = RestrictedQuerySet.as_manager()
  252. class Meta:
  253. ordering = ('cable', 'cable_end', 'pk')
  254. indexes = (
  255. models.Index(fields=('termination_type', 'termination_id')),
  256. )
  257. constraints = (
  258. models.UniqueConstraint(
  259. fields=('termination_type', 'termination_id'),
  260. name='%(app_label)s_%(class)s_unique_termination'
  261. ),
  262. )
  263. verbose_name = _('cable termination')
  264. verbose_name_plural = _('cable terminations')
  265. def __str__(self):
  266. return f'Cable {self.cable} to {self.termination}'
  267. def clean(self):
  268. super().clean()
  269. # Check for existing termination
  270. qs = CableTermination.objects.filter(
  271. termination_type=self.termination_type,
  272. termination_id=self.termination_id
  273. )
  274. if self.cable.pk:
  275. qs = qs.exclude(cable=self.cable)
  276. existing_termination = qs.first()
  277. if existing_termination is not None:
  278. raise ValidationError(
  279. _("Duplicate termination found for {app_label}.{model} {termination_id}: cable {cable_pk}".format(
  280. app_label=self.termination_type.app_label,
  281. model=self.termination_type.model,
  282. termination_id=self.termination_id,
  283. cable_pk=existing_termination.cable.pk
  284. ))
  285. )
  286. # Validate interface type (if applicable)
  287. if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
  288. raise ValidationError(
  289. _("Cables cannot be terminated to {type_display} interfaces").format(
  290. type_display=self.termination.get_type_display()
  291. )
  292. )
  293. # A CircuitTermination attached to a ProviderNetwork cannot have a Cable
  294. if self.termination_type.model == 'circuittermination' and self.termination._provider_network is not None:
  295. raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
  296. def save(self, *args, **kwargs):
  297. # Cache objects associated with the terminating object (for filtering)
  298. self.cache_related_objects()
  299. super().save(*args, **kwargs)
  300. # Set the cable on the terminating object
  301. termination = self.termination._meta.model.objects.get(pk=self.termination_id)
  302. termination.snapshot()
  303. termination.cable = self.cable
  304. termination.cable_end = self.cable_end
  305. termination.save()
  306. def delete(self, *args, **kwargs):
  307. # Delete the cable association on the terminating object
  308. termination = self.termination._meta.model.objects.get(pk=self.termination_id)
  309. termination.snapshot()
  310. termination.cable = None
  311. termination.cable_end = None
  312. termination.save()
  313. super().delete(*args, **kwargs)
  314. def cache_related_objects(self):
  315. """
  316. Cache objects related to the termination (e.g. device, rack, site) directly on the object to
  317. enable efficient filtering.
  318. """
  319. assert self.termination is not None
  320. # Device components
  321. if getattr(self.termination, 'device', None):
  322. self._device = self.termination.device
  323. self._rack = self.termination.device.rack
  324. self._location = self.termination.device.location
  325. self._site = self.termination.device.site
  326. # Power feeds
  327. elif getattr(self.termination, 'rack', None):
  328. self._rack = self.termination.rack
  329. self._location = self.termination.rack.location
  330. self._site = self.termination.rack.site
  331. # Circuit terminations
  332. elif getattr(self.termination, 'site', None):
  333. self._site = self.termination.site
  334. cache_related_objects.alters_data = True
  335. def to_objectchange(self, action):
  336. objectchange = super().to_objectchange(action)
  337. objectchange.related_object = self.termination
  338. return objectchange
  339. class CablePath(models.Model):
  340. """
  341. A CablePath instance represents the physical path from a set of origin nodes to a set of destination nodes,
  342. including all intermediate elements.
  343. `path` contains the ordered set of nodes, arranged in lists of (type, ID) tuples. (Each cable in the path can
  344. terminate to one or more objects.) For example, consider the following
  345. topology:
  346. A B C
  347. Interface 1 --- Front Port 1 | Rear Port 1 --- Rear Port 2 | Front Port 3 --- Interface 2
  348. Front Port 2 Front Port 4
  349. This path would be expressed as:
  350. CablePath(
  351. path = [
  352. [Interface 1],
  353. [Cable A],
  354. [Front Port 1, Front Port 2],
  355. [Rear Port 1],
  356. [Cable B],
  357. [Rear Port 2],
  358. [Front Port 3, Front Port 4],
  359. [Cable C],
  360. [Interface 2],
  361. ]
  362. )
  363. `is_active` is set to True only if every Cable within the path has a status of "connected". `is_complete` is True
  364. if the instance represents a complete end-to-end path from origin(s) to destination(s). `is_split` is True if the
  365. path diverges across multiple cables.
  366. `_nodes` retains a flattened list of all nodes within the path to enable simple filtering.
  367. """
  368. path = models.JSONField(
  369. verbose_name=_('path'),
  370. default=list
  371. )
  372. is_active = models.BooleanField(
  373. verbose_name=_('is active'),
  374. default=False
  375. )
  376. is_complete = models.BooleanField(
  377. verbose_name=_('is complete'),
  378. default=False
  379. )
  380. is_split = models.BooleanField(
  381. verbose_name=_('is split'),
  382. default=False
  383. )
  384. _nodes = PathField()
  385. _netbox_private = True
  386. class Meta:
  387. verbose_name = _('cable path')
  388. verbose_name_plural = _('cable paths')
  389. def __str__(self):
  390. return f"Path #{self.pk}: {len(self.path)} hops"
  391. def save(self, *args, **kwargs):
  392. # Save the flattened nodes list
  393. self._nodes = list(itertools.chain(*self.path))
  394. super().save(*args, **kwargs)
  395. # Record a direct reference to this CablePath on its originating object(s)
  396. origin_model = self.origin_type.model_class()
  397. origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
  398. origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
  399. @property
  400. def origin_type(self):
  401. if self.path:
  402. ct_id, _ = decompile_path_node(self.path[0][0])
  403. return ObjectType.objects.get_for_id(ct_id)
  404. @property
  405. def destination_type(self):
  406. if self.is_complete:
  407. ct_id, _ = decompile_path_node(self.path[-1][0])
  408. return ObjectType.objects.get_for_id(ct_id)
  409. @property
  410. def _path_decompiled(self):
  411. res = []
  412. for step in self.path:
  413. nodes = []
  414. for node in step:
  415. nodes.append(decompile_path_node(node))
  416. res.append(nodes)
  417. return res
  418. path_objects = GenericArrayForeignKey("_path_decompiled")
  419. @property
  420. def origins(self):
  421. """
  422. Return the list of originating objects.
  423. """
  424. return self.path_objects[0]
  425. @property
  426. def destinations(self):
  427. """
  428. Return the list of destination objects, if the path is complete.
  429. """
  430. if not self.is_complete:
  431. return []
  432. return self.path_objects[-1]
  433. @property
  434. def segment_count(self):
  435. return int(len(self.path) / 3)
  436. @classmethod
  437. def from_origin(cls, terminations):
  438. """
  439. Create a new CablePath instance as traced from the given termination objects. These can be any object to which a
  440. Cable or WirelessLink connects (interfaces, console ports, circuit termination, etc.). All terminations must be
  441. of the same type and must belong to the same parent object.
  442. """
  443. from circuits.models import CircuitTermination
  444. if not terminations:
  445. return None
  446. # Ensure all originating terminations are attached to the same link
  447. if len(terminations) > 1:
  448. assert all(t.link == terminations[0].link for t in terminations[1:])
  449. path = []
  450. position_stack = []
  451. is_complete = False
  452. is_active = True
  453. is_split = False
  454. while terminations:
  455. # Terminations must all be of the same type
  456. assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
  457. # All mid-span terminations must all be attached to the same device
  458. if not isinstance(terminations[0], PathEndpoint):
  459. assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
  460. assert all(t.parent_object == terminations[0].parent_object for t in terminations[1:])
  461. # Check for a split path (e.g. rear port fanning out to multiple front ports with
  462. # different cables attached)
  463. if len(set(t.link for t in terminations)) > 1 and (
  464. position_stack and len(terminations) != len(position_stack[-1])
  465. ):
  466. is_split = True
  467. break
  468. # Step 1: Record the near-end termination object(s)
  469. path.append([
  470. object_to_path_node(t) for t in terminations
  471. ])
  472. # Step 2: Determine the attached links (Cable or WirelessLink), if any
  473. links = [termination.link for termination in terminations if termination.link is not None]
  474. if len(links) == 0:
  475. if len(path) == 1:
  476. # If this is the start of the path and no link exists, return None
  477. return None
  478. # Otherwise, halt the trace if no link exists
  479. break
  480. assert all(type(link) in (Cable, WirelessLink) for link in links)
  481. assert all(isinstance(link, type(links[0])) for link in links)
  482. # Step 3: Record asymmetric paths as split
  483. not_connected_terminations = [termination.link for termination in terminations if termination.link is None]
  484. if len(not_connected_terminations) > 0:
  485. is_complete = False
  486. is_split = True
  487. # Step 4: Record the links, keeping cables in order to allow for SVG rendering
  488. cables = []
  489. for link in links:
  490. if object_to_path_node(link) not in cables:
  491. cables.append(object_to_path_node(link))
  492. path.append(cables)
  493. # Step 5: Update the path status if a link is not connected
  494. links_status = [link.status for link in links if link.status != LinkStatusChoices.STATUS_CONNECTED]
  495. if any([status != LinkStatusChoices.STATUS_CONNECTED for status in links_status]):
  496. is_active = False
  497. # Step 6: Determine the far-end terminations
  498. if isinstance(links[0], Cable):
  499. termination_type = ObjectType.objects.get_for_model(terminations[0])
  500. local_cable_terminations = CableTermination.objects.filter(
  501. termination_type=termination_type,
  502. termination_id__in=[t.pk for t in terminations]
  503. )
  504. q_filter = Q()
  505. for lct in local_cable_terminations:
  506. cable_end = 'A' if lct.cable_end == 'B' else 'B'
  507. q_filter |= Q(cable=lct.cable, cable_end=cable_end)
  508. # Make sure this filter has been populated; if not, we have probably been given invalid data
  509. if not q_filter:
  510. break
  511. remote_cable_terminations = CableTermination.objects.filter(q_filter)
  512. remote_terminations = [ct.termination for ct in remote_cable_terminations]
  513. else:
  514. # WirelessLink
  515. remote_terminations = [
  516. link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
  517. ]
  518. # Remote Terminations must all be of the same type, otherwise return a split path
  519. if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
  520. is_complete = False
  521. is_split = True
  522. break
  523. # Step 7: Record the far-end termination object(s)
  524. path.append([
  525. object_to_path_node(t) for t in remote_terminations if t is not None
  526. ])
  527. # Step 8: Determine the "next hop" terminations, if applicable
  528. if not remote_terminations:
  529. break
  530. if isinstance(remote_terminations[0], FrontPort):
  531. # Follow FrontPorts to their corresponding RearPorts
  532. rear_ports = RearPort.objects.filter(
  533. pk__in=[t.rear_port_id for t in remote_terminations]
  534. )
  535. if len(rear_ports) > 1 or rear_ports[0].positions > 1:
  536. position_stack.append([fp.rear_port_position for fp in remote_terminations])
  537. terminations = rear_ports
  538. elif isinstance(remote_terminations[0], RearPort):
  539. if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
  540. front_ports = FrontPort.objects.filter(
  541. rear_port_id__in=[rp.pk for rp in remote_terminations],
  542. rear_port_position=1
  543. )
  544. # Obtain the individual front ports based on the termination and all positions
  545. elif len(remote_terminations) > 1 and position_stack:
  546. positions = position_stack.pop()
  547. # Ensure we have a number of positions equal to the amount of remote terminations
  548. assert len(remote_terminations) == len(positions)
  549. # Get our front ports
  550. q_filter = Q()
  551. for rt in remote_terminations:
  552. position = positions.pop()
  553. q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
  554. assert q_filter is not Q()
  555. front_ports = FrontPort.objects.filter(q_filter)
  556. # Obtain the individual front ports based on the termination and position
  557. elif position_stack:
  558. front_ports = FrontPort.objects.filter(
  559. rear_port_id=remote_terminations[0].pk,
  560. rear_port_position__in=position_stack.pop()
  561. )
  562. # If all rear ports have a single position, we can just get the front ports
  563. elif all([rp.positions == 1 for rp in remote_terminations]):
  564. front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
  565. if len(front_ports) != len(remote_terminations):
  566. # Some rear ports does not have a front port
  567. is_split = True
  568. break
  569. else:
  570. # No position indicated: path has split, so we stop at the RearPorts
  571. is_split = True
  572. break
  573. terminations = front_ports
  574. elif isinstance(remote_terminations[0], CircuitTermination):
  575. # Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
  576. if len(remote_terminations) > 1:
  577. is_split = True
  578. break
  579. circuit_termination = CircuitTermination.objects.filter(
  580. circuit=remote_terminations[0].circuit,
  581. term_side='Z' if remote_terminations[0].term_side == 'A' else 'A'
  582. ).first()
  583. if circuit_termination is None:
  584. break
  585. elif circuit_termination._provider_network:
  586. # Circuit terminates to a ProviderNetwork
  587. path.extend([
  588. [object_to_path_node(circuit_termination)],
  589. [object_to_path_node(circuit_termination._provider_network)],
  590. ])
  591. is_complete = True
  592. break
  593. elif circuit_termination.termination and not circuit_termination.cable:
  594. # Circuit terminates to a Region/Site/etc.
  595. path.extend([
  596. [object_to_path_node(circuit_termination)],
  597. [object_to_path_node(circuit_termination.termination)],
  598. ])
  599. break
  600. terminations = [circuit_termination]
  601. else:
  602. # Check for non-symmetric path
  603. if all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
  604. is_complete = True
  605. elif len(remote_terminations) == 0:
  606. is_complete = False
  607. else:
  608. # Unsupported topology, mark as split and exit
  609. is_complete = False
  610. is_split = True
  611. break
  612. return cls(
  613. path=path,
  614. is_complete=is_complete,
  615. is_active=is_active,
  616. is_split=is_split
  617. )
  618. def retrace(self):
  619. """
  620. Retrace the path from the currently-defined originating termination(s)
  621. """
  622. _new = self.from_origin(self.origins)
  623. if _new:
  624. self.path = _new.path
  625. self.is_complete = _new.is_complete
  626. self.is_active = _new.is_active
  627. self.is_split = _new.is_split
  628. self.save()
  629. else:
  630. self.delete()
  631. retrace.alters_data = True
  632. def get_cable_ids(self):
  633. """
  634. Return all Cable IDs within the path.
  635. """
  636. cable_ct = ObjectType.objects.get_for_model(Cable).pk
  637. cable_ids = []
  638. for node in self._nodes:
  639. ct, id = decompile_path_node(node)
  640. if ct == cable_ct:
  641. cable_ids.append(id)
  642. return cable_ids
  643. def get_total_length(self):
  644. """
  645. Return a tuple containing the sum of the length of each cable in the path
  646. and a flag indicating whether the length is definitive.
  647. """
  648. cable_ids = self.get_cable_ids()
  649. cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
  650. total_length = cables.aggregate(total=Sum('_abs_length'))['total']
  651. is_definitive = len(cables) == len(cable_ids)
  652. return total_length, is_definitive
  653. def get_split_nodes(self):
  654. """
  655. Return all available next segments in a split cable path.
  656. """
  657. from circuits.models import CircuitTermination
  658. nodes = self.path_objects[-1]
  659. # RearPort splitting to multiple FrontPorts with no stack position
  660. if type(nodes[0]) is RearPort:
  661. return FrontPort.objects.filter(rear_port__in=nodes)
  662. # Cable terminating to multiple FrontPorts mapped to different
  663. # RearPorts connected to different cables
  664. elif type(nodes[0]) is FrontPort:
  665. return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
  666. # Cable terminating to multiple CircuitTerminations
  667. elif type(nodes[0]) is CircuitTermination:
  668. return [
  669. ct.get_peer_termination() for ct in nodes
  670. ]
  671. def get_asymmetric_nodes(self):
  672. """
  673. Return all available next segments in a split cable path.
  674. """
  675. from circuits.models import CircuitTermination
  676. asymmetric_nodes = []
  677. for nodes in self.path_objects:
  678. if type(nodes[0]) in [RearPort, FrontPort, CircuitTermination]:
  679. asymmetric_nodes.extend([node for node in nodes if node.link is None])
  680. return asymmetric_nodes