cables.py 30 KB

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