device_components.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  1. from django.contrib.contenttypes.fields import GenericRelation
  2. from django.core.exceptions import ObjectDoesNotExist, ValidationError
  3. from django.core.validators import MaxValueValidator, MinValueValidator
  4. from django.db import models
  5. from django.db.models import Sum
  6. from django.urls import reverse
  7. from taggit.managers import TaggableManager
  8. from dcim.choices import *
  9. from dcim.constants import *
  10. from dcim.exceptions import LoopDetected
  11. from dcim.fields import MACAddressField
  12. from extras.models import ObjectChange, TaggedItem
  13. from utilities.fields import NaturalOrderingField
  14. from utilities.ordering import naturalize_interface
  15. from utilities.utils import serialize_object
  16. from virtualization.choices import VMInterfaceTypeChoices
  17. __all__ = (
  18. 'CableTermination',
  19. 'ConsolePort',
  20. 'ConsoleServerPort',
  21. 'DeviceBay',
  22. 'FrontPort',
  23. 'Interface',
  24. 'InventoryItem',
  25. 'PowerOutlet',
  26. 'PowerPort',
  27. 'RearPort',
  28. )
  29. class ComponentModel(models.Model):
  30. description = models.CharField(
  31. max_length=100,
  32. blank=True
  33. )
  34. class Meta:
  35. abstract = True
  36. def to_objectchange(self, action):
  37. # Annotate the parent Device/VM
  38. try:
  39. parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
  40. except ObjectDoesNotExist:
  41. # The parent device/VM has already been deleted
  42. parent = None
  43. return ObjectChange(
  44. changed_object=self,
  45. object_repr=str(self),
  46. action=action,
  47. related_object=parent,
  48. object_data=serialize_object(self)
  49. )
  50. @property
  51. def parent(self):
  52. return getattr(self, 'device', None)
  53. class CableTermination(models.Model):
  54. cable = models.ForeignKey(
  55. to='dcim.Cable',
  56. on_delete=models.SET_NULL,
  57. related_name='+',
  58. blank=True,
  59. null=True
  60. )
  61. # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
  62. _cabled_as_a = GenericRelation(
  63. to='dcim.Cable',
  64. content_type_field='termination_a_type',
  65. object_id_field='termination_a_id'
  66. )
  67. _cabled_as_b = GenericRelation(
  68. to='dcim.Cable',
  69. content_type_field='termination_b_type',
  70. object_id_field='termination_b_id'
  71. )
  72. is_path_endpoint = True
  73. class Meta:
  74. abstract = True
  75. def trace(self, position=1, follow_circuits=False, cable_history=None):
  76. """
  77. Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
  78. [
  79. (termination A, cable, termination B),
  80. (termination C, cable, termination D),
  81. (termination E, cable, termination F)
  82. ]
  83. """
  84. def get_peer_port(termination, position=1, follow_circuits=False):
  85. from circuits.models import CircuitTermination
  86. # Map a front port to its corresponding rear port
  87. if isinstance(termination, FrontPort):
  88. return termination.rear_port, termination.rear_port_position
  89. # Map a rear port/position to its corresponding front port
  90. elif isinstance(termination, RearPort):
  91. if position not in range(1, termination.positions + 1):
  92. raise Exception("Invalid position for {} ({} positions): {})".format(
  93. termination, termination.positions, position
  94. ))
  95. try:
  96. peer_port = FrontPort.objects.get(
  97. rear_port=termination,
  98. rear_port_position=position,
  99. )
  100. return peer_port, 1
  101. except ObjectDoesNotExist:
  102. return None, None
  103. # Follow a circuit to its other termination
  104. elif isinstance(termination, CircuitTermination) and follow_circuits:
  105. peer_termination = termination.get_peer_termination()
  106. if peer_termination is None:
  107. return None, None
  108. return peer_termination, position
  109. # Termination is not a pass-through port
  110. else:
  111. return None, None
  112. if not self.cable:
  113. return [(self, None, None)]
  114. # Record cable history to detect loops
  115. if cable_history is None:
  116. cable_history = []
  117. elif self.cable in cable_history:
  118. raise LoopDetected()
  119. cable_history.append(self.cable)
  120. far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
  121. path = [(self, self.cable, far_end)]
  122. peer_port, position = get_peer_port(far_end, position, follow_circuits)
  123. if peer_port is None:
  124. return path
  125. try:
  126. next_segment = peer_port.trace(position, follow_circuits, cable_history)
  127. except LoopDetected:
  128. return path
  129. if next_segment is None:
  130. return path + [(peer_port, None, None)]
  131. return path + next_segment
  132. def get_cable_peer(self):
  133. if self.cable is None:
  134. return None
  135. if self._cabled_as_a.exists():
  136. return self.cable.termination_b
  137. if self._cabled_as_b.exists():
  138. return self.cable.termination_a
  139. #
  140. # Console ports
  141. #
  142. class ConsolePort(CableTermination, ComponentModel):
  143. """
  144. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
  145. """
  146. device = models.ForeignKey(
  147. to='dcim.Device',
  148. on_delete=models.CASCADE,
  149. related_name='consoleports'
  150. )
  151. name = models.CharField(
  152. max_length=50
  153. )
  154. _name = NaturalOrderingField(
  155. target_field='name',
  156. max_length=100,
  157. blank=True
  158. )
  159. type = models.CharField(
  160. max_length=50,
  161. choices=ConsolePortTypeChoices,
  162. blank=True
  163. )
  164. connected_endpoint = models.OneToOneField(
  165. to='dcim.ConsoleServerPort',
  166. on_delete=models.SET_NULL,
  167. related_name='connected_endpoint',
  168. blank=True,
  169. null=True
  170. )
  171. connection_status = models.NullBooleanField(
  172. choices=CONNECTION_STATUS_CHOICES,
  173. blank=True
  174. )
  175. tags = TaggableManager(through=TaggedItem)
  176. csv_headers = ['device', 'name', 'type', 'description']
  177. class Meta:
  178. ordering = ('device', '_name')
  179. unique_together = ('device', 'name')
  180. def __str__(self):
  181. return self.name
  182. def get_absolute_url(self):
  183. return self.device.get_absolute_url()
  184. def to_csv(self):
  185. return (
  186. self.device.identifier,
  187. self.name,
  188. self.type,
  189. self.description,
  190. )
  191. #
  192. # Console server ports
  193. #
  194. class ConsoleServerPort(CableTermination, ComponentModel):
  195. """
  196. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
  197. """
  198. device = models.ForeignKey(
  199. to='dcim.Device',
  200. on_delete=models.CASCADE,
  201. related_name='consoleserverports'
  202. )
  203. name = models.CharField(
  204. max_length=50
  205. )
  206. _name = NaturalOrderingField(
  207. target_field='name',
  208. max_length=100,
  209. blank=True
  210. )
  211. type = models.CharField(
  212. max_length=50,
  213. choices=ConsolePortTypeChoices,
  214. blank=True
  215. )
  216. connection_status = models.NullBooleanField(
  217. choices=CONNECTION_STATUS_CHOICES,
  218. blank=True
  219. )
  220. tags = TaggableManager(through=TaggedItem)
  221. csv_headers = ['device', 'name', 'type', 'description']
  222. class Meta:
  223. ordering = ('device', '_name')
  224. unique_together = ('device', 'name')
  225. def __str__(self):
  226. return self.name
  227. def get_absolute_url(self):
  228. return self.device.get_absolute_url()
  229. def to_csv(self):
  230. return (
  231. self.device.identifier,
  232. self.name,
  233. self.type,
  234. self.description,
  235. )
  236. #
  237. # Power ports
  238. #
  239. class PowerPort(CableTermination, ComponentModel):
  240. """
  241. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
  242. """
  243. device = models.ForeignKey(
  244. to='dcim.Device',
  245. on_delete=models.CASCADE,
  246. related_name='powerports'
  247. )
  248. name = models.CharField(
  249. max_length=50
  250. )
  251. _name = NaturalOrderingField(
  252. target_field='name',
  253. max_length=100,
  254. blank=True
  255. )
  256. type = models.CharField(
  257. max_length=50,
  258. choices=PowerPortTypeChoices,
  259. blank=True
  260. )
  261. maximum_draw = models.PositiveSmallIntegerField(
  262. blank=True,
  263. null=True,
  264. validators=[MinValueValidator(1)],
  265. help_text="Maximum power draw (watts)"
  266. )
  267. allocated_draw = models.PositiveSmallIntegerField(
  268. blank=True,
  269. null=True,
  270. validators=[MinValueValidator(1)],
  271. help_text="Allocated power draw (watts)"
  272. )
  273. _connected_poweroutlet = models.OneToOneField(
  274. to='dcim.PowerOutlet',
  275. on_delete=models.SET_NULL,
  276. related_name='connected_endpoint',
  277. blank=True,
  278. null=True
  279. )
  280. _connected_powerfeed = models.OneToOneField(
  281. to='dcim.PowerFeed',
  282. on_delete=models.SET_NULL,
  283. related_name='+',
  284. blank=True,
  285. null=True
  286. )
  287. connection_status = models.NullBooleanField(
  288. choices=CONNECTION_STATUS_CHOICES,
  289. blank=True
  290. )
  291. tags = TaggableManager(through=TaggedItem)
  292. csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
  293. class Meta:
  294. ordering = ('device', '_name')
  295. unique_together = ('device', 'name')
  296. def __str__(self):
  297. return self.name
  298. def get_absolute_url(self):
  299. return self.device.get_absolute_url()
  300. def to_csv(self):
  301. return (
  302. self.device.identifier,
  303. self.name,
  304. self.get_type_display(),
  305. self.maximum_draw,
  306. self.allocated_draw,
  307. self.description,
  308. )
  309. @property
  310. def connected_endpoint(self):
  311. if self._connected_poweroutlet:
  312. return self._connected_poweroutlet
  313. return self._connected_powerfeed
  314. @connected_endpoint.setter
  315. def connected_endpoint(self, value):
  316. # TODO: Fix circular import
  317. from . import PowerFeed
  318. if value is None:
  319. self._connected_poweroutlet = None
  320. self._connected_powerfeed = None
  321. elif isinstance(value, PowerOutlet):
  322. self._connected_poweroutlet = value
  323. self._connected_powerfeed = None
  324. elif isinstance(value, PowerFeed):
  325. self._connected_poweroutlet = None
  326. self._connected_powerfeed = value
  327. else:
  328. raise ValueError(
  329. "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
  330. )
  331. def get_power_draw(self):
  332. """
  333. Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
  334. """
  335. # Calculate aggregate draw of all child power outlets if no numbers have been defined manually
  336. if self.allocated_draw is None and self.maximum_draw is None:
  337. outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
  338. utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
  339. maximum_draw_total=Sum('maximum_draw'),
  340. allocated_draw_total=Sum('allocated_draw'),
  341. )
  342. ret = {
  343. 'allocated': utilization['allocated_draw_total'] or 0,
  344. 'maximum': utilization['maximum_draw_total'] or 0,
  345. 'outlet_count': len(outlet_ids),
  346. 'legs': [],
  347. }
  348. # Calculate per-leg aggregates for three-phase feeds
  349. if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
  350. for leg, leg_name in PowerOutletFeedLegChoices:
  351. outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
  352. utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
  353. maximum_draw_total=Sum('maximum_draw'),
  354. allocated_draw_total=Sum('allocated_draw'),
  355. )
  356. ret['legs'].append({
  357. 'name': leg_name,
  358. 'allocated': utilization['allocated_draw_total'] or 0,
  359. 'maximum': utilization['maximum_draw_total'] or 0,
  360. 'outlet_count': len(outlet_ids),
  361. })
  362. return ret
  363. # Default to administratively defined values
  364. return {
  365. 'allocated': self.allocated_draw or 0,
  366. 'maximum': self.maximum_draw or 0,
  367. 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(),
  368. 'legs': [],
  369. }
  370. #
  371. # Power outlets
  372. #
  373. class PowerOutlet(CableTermination, ComponentModel):
  374. """
  375. A physical power outlet (output) within a Device which provides power to a PowerPort.
  376. """
  377. device = models.ForeignKey(
  378. to='dcim.Device',
  379. on_delete=models.CASCADE,
  380. related_name='poweroutlets'
  381. )
  382. name = models.CharField(
  383. max_length=50
  384. )
  385. _name = NaturalOrderingField(
  386. target_field='name',
  387. max_length=100,
  388. blank=True
  389. )
  390. type = models.CharField(
  391. max_length=50,
  392. choices=PowerOutletTypeChoices,
  393. blank=True
  394. )
  395. power_port = models.ForeignKey(
  396. to='dcim.PowerPort',
  397. on_delete=models.SET_NULL,
  398. blank=True,
  399. null=True,
  400. related_name='poweroutlets'
  401. )
  402. feed_leg = models.CharField(
  403. max_length=50,
  404. choices=PowerOutletFeedLegChoices,
  405. blank=True,
  406. help_text="Phase (for three-phase feeds)"
  407. )
  408. connection_status = models.NullBooleanField(
  409. choices=CONNECTION_STATUS_CHOICES,
  410. blank=True
  411. )
  412. tags = TaggableManager(through=TaggedItem)
  413. csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
  414. class Meta:
  415. ordering = ('device', '_name')
  416. unique_together = ('device', 'name')
  417. def __str__(self):
  418. return self.name
  419. def get_absolute_url(self):
  420. return self.device.get_absolute_url()
  421. def to_csv(self):
  422. return (
  423. self.device.identifier,
  424. self.name,
  425. self.get_type_display(),
  426. self.power_port.name if self.power_port else None,
  427. self.get_feed_leg_display(),
  428. self.description,
  429. )
  430. def clean(self):
  431. # Validate power port assignment
  432. if self.power_port and self.power_port.device != self.device:
  433. raise ValidationError(
  434. "Parent power port ({}) must belong to the same device".format(self.power_port)
  435. )
  436. #
  437. # Interfaces
  438. #
  439. class Interface(CableTermination, ComponentModel):
  440. """
  441. A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
  442. Interface.
  443. """
  444. device = models.ForeignKey(
  445. to='Device',
  446. on_delete=models.CASCADE,
  447. related_name='interfaces',
  448. null=True,
  449. blank=True
  450. )
  451. virtual_machine = models.ForeignKey(
  452. to='virtualization.VirtualMachine',
  453. on_delete=models.CASCADE,
  454. related_name='interfaces',
  455. null=True,
  456. blank=True
  457. )
  458. name = models.CharField(
  459. max_length=64
  460. )
  461. _name = NaturalOrderingField(
  462. target_field='name',
  463. naturalize_function=naturalize_interface,
  464. max_length=100,
  465. blank=True
  466. )
  467. _connected_interface = models.OneToOneField(
  468. to='self',
  469. on_delete=models.SET_NULL,
  470. related_name='+',
  471. blank=True,
  472. null=True
  473. )
  474. _connected_circuittermination = models.OneToOneField(
  475. to='circuits.CircuitTermination',
  476. on_delete=models.SET_NULL,
  477. related_name='+',
  478. blank=True,
  479. null=True
  480. )
  481. connection_status = models.NullBooleanField(
  482. choices=CONNECTION_STATUS_CHOICES,
  483. blank=True
  484. )
  485. lag = models.ForeignKey(
  486. to='self',
  487. on_delete=models.SET_NULL,
  488. related_name='member_interfaces',
  489. null=True,
  490. blank=True,
  491. verbose_name='Parent LAG'
  492. )
  493. type = models.CharField(
  494. max_length=50,
  495. choices=InterfaceTypeChoices
  496. )
  497. enabled = models.BooleanField(
  498. default=True
  499. )
  500. mac_address = MACAddressField(
  501. null=True,
  502. blank=True,
  503. verbose_name='MAC Address'
  504. )
  505. mtu = models.PositiveIntegerField(
  506. blank=True,
  507. null=True,
  508. validators=[MinValueValidator(1), MaxValueValidator(65536)],
  509. verbose_name='MTU'
  510. )
  511. mgmt_only = models.BooleanField(
  512. default=False,
  513. verbose_name='OOB Management',
  514. help_text='This interface is used only for out-of-band management'
  515. )
  516. mode = models.CharField(
  517. max_length=50,
  518. choices=InterfaceModeChoices,
  519. blank=True,
  520. )
  521. untagged_vlan = models.ForeignKey(
  522. to='ipam.VLAN',
  523. on_delete=models.SET_NULL,
  524. related_name='interfaces_as_untagged',
  525. null=True,
  526. blank=True,
  527. verbose_name='Untagged VLAN'
  528. )
  529. tagged_vlans = models.ManyToManyField(
  530. to='ipam.VLAN',
  531. related_name='interfaces_as_tagged',
  532. blank=True,
  533. verbose_name='Tagged VLANs'
  534. )
  535. tags = TaggableManager(through=TaggedItem)
  536. csv_headers = [
  537. 'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
  538. 'description', 'mode',
  539. ]
  540. class Meta:
  541. # TODO: ordering and unique_together should include virtual_machine
  542. ordering = ('device', '_name')
  543. unique_together = ('device', 'name')
  544. def __str__(self):
  545. return self.name
  546. def get_absolute_url(self):
  547. return reverse('dcim:interface', kwargs={'pk': self.pk})
  548. def to_csv(self):
  549. return (
  550. self.device.identifier if self.device else None,
  551. self.virtual_machine.name if self.virtual_machine else None,
  552. self.name,
  553. self.lag.name if self.lag else None,
  554. self.get_type_display(),
  555. self.enabled,
  556. self.mac_address,
  557. self.mtu,
  558. self.mgmt_only,
  559. self.description,
  560. self.get_mode_display(),
  561. )
  562. def clean(self):
  563. # An Interface must belong to a Device *or* to a VirtualMachine
  564. if self.device and self.virtual_machine:
  565. raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
  566. if not self.device and not self.virtual_machine:
  567. raise ValidationError("An interface must belong to either a device or a virtual machine.")
  568. # VM interfaces must be virtual
  569. if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values():
  570. raise ValidationError({
  571. 'type': "Invalid interface type for a virtual machine: {}".format(self.type)
  572. })
  573. # Virtual interfaces cannot be connected
  574. if self.type in NONCONNECTABLE_IFACE_TYPES and (
  575. self.cable or getattr(self, 'circuit_termination', False)
  576. ):
  577. raise ValidationError({
  578. 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
  579. "Disconnect the interface or choose a suitable type."
  580. })
  581. # An interface's LAG must belong to the same device (or VC master)
  582. if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]:
  583. raise ValidationError({
  584. 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format(
  585. self.lag.name, self.lag.device.name
  586. )
  587. })
  588. # A virtual interface cannot have a parent LAG
  589. if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
  590. raise ValidationError({
  591. 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display())
  592. })
  593. # Only a LAG can have LAG members
  594. if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists():
  595. raise ValidationError({
  596. 'type': "Cannot change interface type; it has LAG members ({}).".format(
  597. ", ".join([iface.name for iface in self.member_interfaces.all()])
  598. )
  599. })
  600. # Validate untagged VLAN
  601. if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
  602. raise ValidationError({
  603. 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
  604. "device/VM, or it must be global".format(self.untagged_vlan)
  605. })
  606. def save(self, *args, **kwargs):
  607. # Remove untagged VLAN assignment for non-802.1Q interfaces
  608. if self.mode is None:
  609. self.untagged_vlan = None
  610. # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
  611. if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
  612. self.tagged_vlans.clear()
  613. return super().save(*args, **kwargs)
  614. def to_objectchange(self, action):
  615. # Annotate the parent Device/VM
  616. try:
  617. parent_obj = self.device or self.virtual_machine
  618. except ObjectDoesNotExist:
  619. parent_obj = None
  620. return ObjectChange(
  621. changed_object=self,
  622. object_repr=str(self),
  623. action=action,
  624. related_object=parent_obj,
  625. object_data=serialize_object(self)
  626. )
  627. @property
  628. def connected_endpoint(self):
  629. if self._connected_interface:
  630. return self._connected_interface
  631. return self._connected_circuittermination
  632. @connected_endpoint.setter
  633. def connected_endpoint(self, value):
  634. from circuits.models import CircuitTermination
  635. if value is None:
  636. self._connected_interface = None
  637. self._connected_circuittermination = None
  638. elif isinstance(value, Interface):
  639. self._connected_interface = value
  640. self._connected_circuittermination = None
  641. elif isinstance(value, CircuitTermination):
  642. self._connected_interface = None
  643. self._connected_circuittermination = value
  644. else:
  645. raise ValueError(
  646. "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value))
  647. )
  648. @property
  649. def parent(self):
  650. return self.device or self.virtual_machine
  651. @property
  652. def is_connectable(self):
  653. return self.type not in NONCONNECTABLE_IFACE_TYPES
  654. @property
  655. def is_virtual(self):
  656. return self.type in VIRTUAL_IFACE_TYPES
  657. @property
  658. def is_wireless(self):
  659. return self.type in WIRELESS_IFACE_TYPES
  660. @property
  661. def is_lag(self):
  662. return self.type == InterfaceTypeChoices.TYPE_LAG
  663. @property
  664. def count_ipaddresses(self):
  665. return self.ip_addresses.count()
  666. #
  667. # Pass-through ports
  668. #
  669. class FrontPort(CableTermination, ComponentModel):
  670. """
  671. A pass-through port on the front of a Device.
  672. """
  673. device = models.ForeignKey(
  674. to='dcim.Device',
  675. on_delete=models.CASCADE,
  676. related_name='frontports'
  677. )
  678. name = models.CharField(
  679. max_length=64
  680. )
  681. _name = NaturalOrderingField(
  682. target_field='name',
  683. max_length=100,
  684. blank=True
  685. )
  686. type = models.CharField(
  687. max_length=50,
  688. choices=PortTypeChoices
  689. )
  690. rear_port = models.ForeignKey(
  691. to='dcim.RearPort',
  692. on_delete=models.CASCADE,
  693. related_name='frontports'
  694. )
  695. rear_port_position = models.PositiveSmallIntegerField(
  696. default=1,
  697. validators=[MinValueValidator(1), MaxValueValidator(64)]
  698. )
  699. tags = TaggableManager(through=TaggedItem)
  700. csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
  701. is_path_endpoint = False
  702. class Meta:
  703. ordering = ('device', '_name')
  704. unique_together = (
  705. ('device', 'name'),
  706. ('rear_port', 'rear_port_position'),
  707. )
  708. def __str__(self):
  709. return self.name
  710. def to_csv(self):
  711. return (
  712. self.device.identifier,
  713. self.name,
  714. self.get_type_display(),
  715. self.rear_port.name,
  716. self.rear_port_position,
  717. self.description,
  718. )
  719. def clean(self):
  720. # Validate rear port assignment
  721. if self.rear_port.device != self.device:
  722. raise ValidationError(
  723. "Rear port ({}) must belong to the same device".format(self.rear_port)
  724. )
  725. # Validate rear port position assignment
  726. if self.rear_port_position > self.rear_port.positions:
  727. raise ValidationError(
  728. "Invalid rear port position ({}); rear port {} has only {} positions".format(
  729. self.rear_port_position, self.rear_port.name, self.rear_port.positions
  730. )
  731. )
  732. class RearPort(CableTermination, ComponentModel):
  733. """
  734. A pass-through port on the rear of a Device.
  735. """
  736. device = models.ForeignKey(
  737. to='dcim.Device',
  738. on_delete=models.CASCADE,
  739. related_name='rearports'
  740. )
  741. name = models.CharField(
  742. max_length=64
  743. )
  744. _name = NaturalOrderingField(
  745. target_field='name',
  746. max_length=100,
  747. blank=True
  748. )
  749. type = models.CharField(
  750. max_length=50,
  751. choices=PortTypeChoices
  752. )
  753. positions = models.PositiveSmallIntegerField(
  754. default=1,
  755. validators=[MinValueValidator(1), MaxValueValidator(64)]
  756. )
  757. tags = TaggableManager(through=TaggedItem)
  758. csv_headers = ['device', 'name', 'type', 'positions', 'description']
  759. is_path_endpoint = False
  760. class Meta:
  761. ordering = ('device', '_name')
  762. unique_together = ('device', 'name')
  763. def __str__(self):
  764. return self.name
  765. def to_csv(self):
  766. return (
  767. self.device.identifier,
  768. self.name,
  769. self.get_type_display(),
  770. self.positions,
  771. self.description,
  772. )
  773. #
  774. # Device bays
  775. #
  776. class DeviceBay(ComponentModel):
  777. """
  778. An empty space within a Device which can house a child device
  779. """
  780. device = models.ForeignKey(
  781. to='dcim.Device',
  782. on_delete=models.CASCADE,
  783. related_name='device_bays'
  784. )
  785. name = models.CharField(
  786. max_length=50,
  787. verbose_name='Name'
  788. )
  789. _name = NaturalOrderingField(
  790. target_field='name',
  791. max_length=100,
  792. blank=True
  793. )
  794. installed_device = models.OneToOneField(
  795. to='dcim.Device',
  796. on_delete=models.SET_NULL,
  797. related_name='parent_bay',
  798. blank=True,
  799. null=True
  800. )
  801. tags = TaggableManager(through=TaggedItem)
  802. csv_headers = ['device', 'name', 'installed_device', 'description']
  803. class Meta:
  804. ordering = ('device', '_name')
  805. unique_together = ('device', 'name')
  806. def __str__(self):
  807. return '{} - {}'.format(self.device.name, self.name)
  808. def get_absolute_url(self):
  809. return self.device.get_absolute_url()
  810. def to_csv(self):
  811. return (
  812. self.device.identifier,
  813. self.name,
  814. self.installed_device.identifier if self.installed_device else None,
  815. self.description,
  816. )
  817. def clean(self):
  818. # Validate that the parent Device can have DeviceBays
  819. if not self.device.device_type.is_parent_device:
  820. raise ValidationError("This type of device ({}) does not support device bays.".format(
  821. self.device.device_type
  822. ))
  823. # Cannot install a device into itself, obviously
  824. if self.device == self.installed_device:
  825. raise ValidationError("Cannot install a device into itself.")
  826. # Check that the installed device is not already installed elsewhere
  827. if self.installed_device:
  828. current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
  829. if current_bay and current_bay != self:
  830. raise ValidationError({
  831. 'installed_device': "Cannot install the specified device; device is already installed in {}".format(
  832. current_bay
  833. )
  834. })
  835. #
  836. # Inventory items
  837. #
  838. class InventoryItem(ComponentModel):
  839. """
  840. An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
  841. InventoryItems are used only for inventory purposes.
  842. """
  843. device = models.ForeignKey(
  844. to='dcim.Device',
  845. on_delete=models.CASCADE,
  846. related_name='inventory_items'
  847. )
  848. parent = models.ForeignKey(
  849. to='self',
  850. on_delete=models.CASCADE,
  851. related_name='child_items',
  852. blank=True,
  853. null=True
  854. )
  855. name = models.CharField(
  856. max_length=50,
  857. verbose_name='Name'
  858. )
  859. _name = NaturalOrderingField(
  860. target_field='name',
  861. max_length=100,
  862. blank=True
  863. )
  864. manufacturer = models.ForeignKey(
  865. to='dcim.Manufacturer',
  866. on_delete=models.PROTECT,
  867. related_name='inventory_items',
  868. blank=True,
  869. null=True
  870. )
  871. part_id = models.CharField(
  872. max_length=50,
  873. verbose_name='Part ID',
  874. blank=True
  875. )
  876. serial = models.CharField(
  877. max_length=50,
  878. verbose_name='Serial number',
  879. blank=True
  880. )
  881. asset_tag = models.CharField(
  882. max_length=50,
  883. unique=True,
  884. blank=True,
  885. null=True,
  886. verbose_name='Asset tag',
  887. help_text='A unique tag used to identify this item'
  888. )
  889. discovered = models.BooleanField(
  890. default=False,
  891. verbose_name='Discovered'
  892. )
  893. tags = TaggableManager(through=TaggedItem)
  894. csv_headers = [
  895. 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
  896. ]
  897. class Meta:
  898. ordering = ('device__id', 'parent__id', '_name')
  899. unique_together = ('device', 'parent', 'name')
  900. def __str__(self):
  901. return self.name
  902. def get_absolute_url(self):
  903. return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk})
  904. def to_csv(self):
  905. return (
  906. self.device.name or '{{{}}}'.format(self.device.pk),
  907. self.name,
  908. self.manufacturer.name if self.manufacturer else None,
  909. self.part_id,
  910. self.serial,
  911. self.asset_tag,
  912. self.discovered,
  913. self.description,
  914. )