device_component_templates.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. from django.core.exceptions import ObjectDoesNotExist, ValidationError
  2. from django.core.validators import MaxValueValidator, MinValueValidator
  3. from django.db import models
  4. from dcim.choices import *
  5. from dcim.constants import *
  6. from extras.utils import extras_features
  7. from netbox.models import ChangeLoggedModel
  8. from utilities.fields import ColorField, NaturalOrderingField
  9. from utilities.ordering import naturalize_interface
  10. from .device_components import (
  11. ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, ModuleBay, PowerOutlet, PowerPort, RearPort,
  12. )
  13. __all__ = (
  14. 'ConsolePortTemplate',
  15. 'ConsoleServerPortTemplate',
  16. 'DeviceBayTemplate',
  17. 'FrontPortTemplate',
  18. 'InterfaceTemplate',
  19. 'ModuleBayTemplate',
  20. 'PowerOutletTemplate',
  21. 'PowerPortTemplate',
  22. 'RearPortTemplate',
  23. )
  24. class ComponentTemplateModel(ChangeLoggedModel):
  25. device_type = models.ForeignKey(
  26. to='dcim.DeviceType',
  27. on_delete=models.CASCADE,
  28. related_name='%(class)ss'
  29. )
  30. name = models.CharField(
  31. max_length=64
  32. )
  33. _name = NaturalOrderingField(
  34. target_field='name',
  35. max_length=100,
  36. blank=True
  37. )
  38. label = models.CharField(
  39. max_length=64,
  40. blank=True,
  41. help_text="Physical label"
  42. )
  43. description = models.CharField(
  44. max_length=200,
  45. blank=True
  46. )
  47. class Meta:
  48. abstract = True
  49. def __str__(self):
  50. if self.label:
  51. return f"{self.name} ({self.label})"
  52. return self.name
  53. def instantiate(self, device):
  54. """
  55. Instantiate a new component on the specified Device.
  56. """
  57. raise NotImplementedError()
  58. def to_objectchange(self, action, related_object=None):
  59. # Annotate the parent DeviceType
  60. try:
  61. device_type = self.device_type
  62. except ObjectDoesNotExist:
  63. # The parent DeviceType has already been deleted
  64. device_type = None
  65. return super().to_objectchange(action, related_object=device_type)
  66. class ModularComponentTemplateModel(ComponentTemplateModel):
  67. """
  68. A ComponentTemplateModel which supports optional assignment to a ModuleType.
  69. """
  70. device_type = models.ForeignKey(
  71. to='dcim.DeviceType',
  72. on_delete=models.CASCADE,
  73. related_name='%(class)ss',
  74. blank=True,
  75. null=True
  76. )
  77. module_type = models.ForeignKey(
  78. to='dcim.ModuleType',
  79. on_delete=models.CASCADE,
  80. related_name='%(class)ss',
  81. blank=True,
  82. null=True
  83. )
  84. class Meta:
  85. abstract = True
  86. def to_objectchange(self, action, related_object=None):
  87. # Annotate the parent DeviceType or ModuleType
  88. try:
  89. if getattr(self, 'device_type'):
  90. return super().to_objectchange(action, related_object=self.device_type)
  91. except ObjectDoesNotExist:
  92. pass
  93. try:
  94. if getattr(self, 'module_type'):
  95. return super().to_objectchange(action, related_object=self.module_type)
  96. except ObjectDoesNotExist:
  97. pass
  98. return super().to_objectchange(action)
  99. def clean(self):
  100. super().clean()
  101. # A component template must belong to a DeviceType *or* to a ModuleType
  102. if self.device_type and self.module_type:
  103. raise ValidationError(
  104. "A component template cannot be associated with both a device type and a module type."
  105. )
  106. if not self.device_type and not self.module_type:
  107. raise ValidationError(
  108. "A component template must be associated with either a device type or a module type."
  109. )
  110. @extras_features('webhooks')
  111. class ConsolePortTemplate(ModularComponentTemplateModel):
  112. """
  113. A template for a ConsolePort to be created for a new Device.
  114. """
  115. type = models.CharField(
  116. max_length=50,
  117. choices=ConsolePortTypeChoices,
  118. blank=True
  119. )
  120. class Meta:
  121. ordering = ('device_type', 'module_type', '_name')
  122. unique_together = (
  123. ('device_type', 'name'),
  124. ('module_type', 'name'),
  125. )
  126. def instantiate(self, **kwargs):
  127. return ConsolePort(
  128. name=self.name,
  129. label=self.label,
  130. type=self.type,
  131. **kwargs
  132. )
  133. @extras_features('webhooks')
  134. class ConsoleServerPortTemplate(ModularComponentTemplateModel):
  135. """
  136. A template for a ConsoleServerPort to be created for a new Device.
  137. """
  138. type = models.CharField(
  139. max_length=50,
  140. choices=ConsolePortTypeChoices,
  141. blank=True
  142. )
  143. class Meta:
  144. ordering = ('device_type', 'module_type', '_name')
  145. unique_together = (
  146. ('device_type', 'name'),
  147. ('module_type', 'name'),
  148. )
  149. def instantiate(self, **kwargs):
  150. return ConsoleServerPort(
  151. name=self.name,
  152. label=self.label,
  153. type=self.type,
  154. **kwargs
  155. )
  156. @extras_features('webhooks')
  157. class PowerPortTemplate(ModularComponentTemplateModel):
  158. """
  159. A template for a PowerPort to be created for a new Device.
  160. """
  161. type = models.CharField(
  162. max_length=50,
  163. choices=PowerPortTypeChoices,
  164. blank=True
  165. )
  166. maximum_draw = models.PositiveSmallIntegerField(
  167. blank=True,
  168. null=True,
  169. validators=[MinValueValidator(1)],
  170. help_text="Maximum power draw (watts)"
  171. )
  172. allocated_draw = models.PositiveSmallIntegerField(
  173. blank=True,
  174. null=True,
  175. validators=[MinValueValidator(1)],
  176. help_text="Allocated power draw (watts)"
  177. )
  178. class Meta:
  179. ordering = ('device_type', 'module_type', '_name')
  180. unique_together = (
  181. ('device_type', 'name'),
  182. ('module_type', 'name'),
  183. )
  184. def instantiate(self, **kwargs):
  185. return PowerPort(
  186. name=self.name,
  187. label=self.label,
  188. type=self.type,
  189. maximum_draw=self.maximum_draw,
  190. allocated_draw=self.allocated_draw,
  191. **kwargs
  192. )
  193. def clean(self):
  194. super().clean()
  195. if self.maximum_draw is not None and self.allocated_draw is not None:
  196. if self.allocated_draw > self.maximum_draw:
  197. raise ValidationError({
  198. 'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
  199. })
  200. @extras_features('webhooks')
  201. class PowerOutletTemplate(ModularComponentTemplateModel):
  202. """
  203. A template for a PowerOutlet to be created for a new Device.
  204. """
  205. type = models.CharField(
  206. max_length=50,
  207. choices=PowerOutletTypeChoices,
  208. blank=True
  209. )
  210. power_port = models.ForeignKey(
  211. to='dcim.PowerPortTemplate',
  212. on_delete=models.SET_NULL,
  213. blank=True,
  214. null=True,
  215. related_name='poweroutlet_templates'
  216. )
  217. feed_leg = models.CharField(
  218. max_length=50,
  219. choices=PowerOutletFeedLegChoices,
  220. blank=True,
  221. help_text="Phase (for three-phase feeds)"
  222. )
  223. class Meta:
  224. ordering = ('device_type', 'module_type', '_name')
  225. unique_together = (
  226. ('device_type', 'name'),
  227. ('module_type', 'name'),
  228. )
  229. def clean(self):
  230. super().clean()
  231. # Validate power port assignment
  232. if self.power_port:
  233. if self.device_type and self.power_port.device_type != self.device_type:
  234. raise ValidationError(
  235. f"Parent power port ({self.power_port}) must belong to the same device type"
  236. )
  237. if self.module_type and self.power_port.module_type != self.module_type:
  238. raise ValidationError(
  239. f"Parent power port ({self.power_port}) must belong to the same module type"
  240. )
  241. def instantiate(self, **kwargs):
  242. if self.power_port:
  243. power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
  244. else:
  245. power_port = None
  246. return PowerOutlet(
  247. name=self.name,
  248. label=self.label,
  249. type=self.type,
  250. power_port=power_port,
  251. feed_leg=self.feed_leg,
  252. **kwargs
  253. )
  254. @extras_features('webhooks')
  255. class InterfaceTemplate(ModularComponentTemplateModel):
  256. """
  257. A template for a physical data interface on a new Device.
  258. """
  259. # Override ComponentTemplateModel._name to specify naturalize_interface function
  260. _name = NaturalOrderingField(
  261. target_field='name',
  262. naturalize_function=naturalize_interface,
  263. max_length=100,
  264. blank=True
  265. )
  266. type = models.CharField(
  267. max_length=50,
  268. choices=InterfaceTypeChoices
  269. )
  270. mgmt_only = models.BooleanField(
  271. default=False,
  272. verbose_name='Management only'
  273. )
  274. class Meta:
  275. ordering = ('device_type', 'module_type', '_name')
  276. unique_together = (
  277. ('device_type', 'name'),
  278. ('module_type', 'name'),
  279. )
  280. def instantiate(self, **kwargs):
  281. return Interface(
  282. name=self.name,
  283. label=self.label,
  284. type=self.type,
  285. mgmt_only=self.mgmt_only,
  286. **kwargs
  287. )
  288. @extras_features('webhooks')
  289. class FrontPortTemplate(ModularComponentTemplateModel):
  290. """
  291. Template for a pass-through port on the front of a new Device.
  292. """
  293. type = models.CharField(
  294. max_length=50,
  295. choices=PortTypeChoices
  296. )
  297. color = ColorField(
  298. blank=True
  299. )
  300. rear_port = models.ForeignKey(
  301. to='dcim.RearPortTemplate',
  302. on_delete=models.CASCADE,
  303. related_name='frontport_templates'
  304. )
  305. rear_port_position = models.PositiveSmallIntegerField(
  306. default=1,
  307. validators=[
  308. MinValueValidator(REARPORT_POSITIONS_MIN),
  309. MaxValueValidator(REARPORT_POSITIONS_MAX)
  310. ]
  311. )
  312. class Meta:
  313. ordering = ('device_type', 'module_type', '_name')
  314. unique_together = (
  315. ('device_type', 'name'),
  316. ('module_type', 'name'),
  317. ('rear_port', 'rear_port_position'),
  318. )
  319. def clean(self):
  320. super().clean()
  321. try:
  322. # Validate rear port assignment
  323. if self.rear_port.device_type != self.device_type:
  324. raise ValidationError(
  325. "Rear port ({}) must belong to the same device type".format(self.rear_port)
  326. )
  327. # Validate rear port position assignment
  328. if self.rear_port_position > self.rear_port.positions:
  329. raise ValidationError(
  330. "Invalid rear port position ({}); rear port {} has only {} positions".format(
  331. self.rear_port_position, self.rear_port.name, self.rear_port.positions
  332. )
  333. )
  334. except RearPortTemplate.DoesNotExist:
  335. pass
  336. def instantiate(self, **kwargs):
  337. if self.rear_port:
  338. rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs)
  339. else:
  340. rear_port = None
  341. return FrontPort(
  342. name=self.name,
  343. label=self.label,
  344. type=self.type,
  345. color=self.color,
  346. rear_port=rear_port,
  347. rear_port_position=self.rear_port_position,
  348. **kwargs
  349. )
  350. @extras_features('webhooks')
  351. class RearPortTemplate(ModularComponentTemplateModel):
  352. """
  353. Template for a pass-through port on the rear of a new Device.
  354. """
  355. type = models.CharField(
  356. max_length=50,
  357. choices=PortTypeChoices
  358. )
  359. color = ColorField(
  360. blank=True
  361. )
  362. positions = models.PositiveSmallIntegerField(
  363. default=1,
  364. validators=[
  365. MinValueValidator(REARPORT_POSITIONS_MIN),
  366. MaxValueValidator(REARPORT_POSITIONS_MAX)
  367. ]
  368. )
  369. class Meta:
  370. ordering = ('device_type', 'module_type', '_name')
  371. unique_together = (
  372. ('device_type', 'name'),
  373. ('module_type', 'name'),
  374. )
  375. def instantiate(self, **kwargs):
  376. return RearPort(
  377. name=self.name,
  378. label=self.label,
  379. type=self.type,
  380. color=self.color,
  381. positions=self.positions,
  382. **kwargs
  383. )
  384. @extras_features('webhooks')
  385. class ModuleBayTemplate(ComponentTemplateModel):
  386. """
  387. A template for a ModuleBay to be created for a new parent Device.
  388. """
  389. class Meta:
  390. ordering = ('device_type', '_name')
  391. unique_together = ('device_type', 'name')
  392. def instantiate(self, device):
  393. return ModuleBay(
  394. device=device,
  395. name=self.name,
  396. label=self.label
  397. )
  398. @extras_features('webhooks')
  399. class DeviceBayTemplate(ComponentTemplateModel):
  400. """
  401. A template for a DeviceBay to be created for a new parent Device.
  402. """
  403. class Meta:
  404. ordering = ('device_type', '_name')
  405. unique_together = ('device_type', 'name')
  406. def instantiate(self, device):
  407. return DeviceBay(
  408. device=device,
  409. name=self.name,
  410. label=self.label
  411. )
  412. def clean(self):
  413. if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
  414. raise ValidationError(
  415. f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
  416. )