device_component_templates.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. from django.contrib.contenttypes.fields import GenericForeignKey
  2. from django.core.exceptions import ValidationError
  3. from django.core.validators import MaxValueValidator, MinValueValidator
  4. from django.db import models
  5. from django.utils.translation import gettext_lazy as _
  6. from mptt.models import MPTTModel, TreeForeignKey
  7. from dcim.choices import *
  8. from dcim.constants import *
  9. from dcim.models.base import PortMappingBase
  10. from dcim.models.mixins import InterfaceValidationMixin
  11. from netbox.models import ChangeLoggedModel
  12. from utilities.fields import ColorField, NaturalOrderingField
  13. from utilities.mptt import TreeManager
  14. from utilities.ordering import naturalize_interface
  15. from utilities.tracking import TrackingModelMixin
  16. from wireless.choices import WirelessRoleChoices
  17. from .device_components import (
  18. ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
  19. RearPort,
  20. )
  21. __all__ = (
  22. 'ConsolePortTemplate',
  23. 'ConsoleServerPortTemplate',
  24. 'DeviceBayTemplate',
  25. 'FrontPortTemplate',
  26. 'InterfaceTemplate',
  27. 'InventoryItemTemplate',
  28. 'ModuleBayTemplate',
  29. 'PortTemplateMapping',
  30. 'PowerOutletTemplate',
  31. 'PowerPortTemplate',
  32. 'RearPortTemplate',
  33. )
  34. class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
  35. device_type = models.ForeignKey(
  36. to='dcim.DeviceType',
  37. on_delete=models.CASCADE,
  38. related_name='%(class)ss'
  39. )
  40. name = models.CharField(
  41. verbose_name=_('name'),
  42. max_length=64,
  43. help_text=_(
  44. "{module} is accepted as a substitution for the module bay position when attached to a module type."
  45. ),
  46. db_collation="natural_sort"
  47. )
  48. label = models.CharField(
  49. verbose_name=_('label'),
  50. max_length=64,
  51. blank=True,
  52. help_text=_('Physical label')
  53. )
  54. description = models.CharField(
  55. verbose_name=_('description'),
  56. max_length=200,
  57. blank=True
  58. )
  59. class Meta:
  60. abstract = True
  61. ordering = ('device_type', 'name')
  62. constraints = (
  63. models.UniqueConstraint(
  64. fields=('device_type', 'name'),
  65. name='%(app_label)s_%(class)s_unique_device_type_name'
  66. ),
  67. )
  68. def __str__(self):
  69. if self.label:
  70. return f"{self.name} ({self.label})"
  71. return self.name
  72. def instantiate(self, device):
  73. """
  74. Instantiate a new component on the specified Device.
  75. """
  76. raise NotImplementedError()
  77. def __init__(self, *args, **kwargs):
  78. super().__init__(*args, **kwargs)
  79. # Cache the original DeviceType ID for reference under clean()
  80. self._original_device_type = self.__dict__.get('device_type_id')
  81. def to_objectchange(self, action):
  82. objectchange = super().to_objectchange(action)
  83. objectchange.related_object = self.device_type
  84. return objectchange
  85. def clean(self):
  86. super().clean()
  87. if not self._state.adding and self._original_device_type != self.device_type_id:
  88. raise ValidationError({
  89. "device_type": _("Component templates cannot be moved to a different device type.")
  90. })
  91. class ModularComponentTemplateModel(ComponentTemplateModel):
  92. """
  93. A ComponentTemplateModel which supports optional assignment to a ModuleType.
  94. """
  95. device_type = models.ForeignKey(
  96. to='dcim.DeviceType',
  97. on_delete=models.CASCADE,
  98. related_name='%(class)ss',
  99. blank=True,
  100. null=True
  101. )
  102. module_type = models.ForeignKey(
  103. to='dcim.ModuleType',
  104. on_delete=models.CASCADE,
  105. related_name='%(class)ss',
  106. blank=True,
  107. null=True
  108. )
  109. class Meta:
  110. abstract = True
  111. ordering = ('device_type', 'module_type', 'name')
  112. constraints = (
  113. models.UniqueConstraint(
  114. fields=('device_type', 'name'),
  115. name='%(app_label)s_%(class)s_unique_device_type_name'
  116. ),
  117. models.UniqueConstraint(
  118. fields=('module_type', 'name'),
  119. name='%(app_label)s_%(class)s_unique_module_type_name'
  120. ),
  121. )
  122. def to_objectchange(self, action):
  123. objectchange = super().to_objectchange(action)
  124. if self.device_type is not None:
  125. objectchange.related_object = self.device_type
  126. elif self.module_type is not None:
  127. objectchange.related_object = self.module_type
  128. return objectchange
  129. def clean(self):
  130. super().clean()
  131. # A component template must belong to a DeviceType *or* to a ModuleType
  132. if self.device_type and self.module_type:
  133. raise ValidationError(
  134. _("A component template cannot be associated with both a device type and a module type.")
  135. )
  136. if not self.device_type and not self.module_type:
  137. raise ValidationError(
  138. _("A component template must be associated with either a device type or a module type.")
  139. )
  140. def _get_module_tree(self, module):
  141. modules = []
  142. while module:
  143. modules.append(module)
  144. if module.module_bay:
  145. module = module.module_bay.module
  146. else:
  147. module = None
  148. modules.reverse()
  149. return modules
  150. def resolve_name(self, module):
  151. if MODULE_TOKEN not in self.name:
  152. return self.name
  153. if module:
  154. modules = self._get_module_tree(module)
  155. token_count = self.name.count(MODULE_TOKEN)
  156. name = self.name
  157. if token_count == 1:
  158. # Single token: substitute with full path (e.g., "1/1" for depth 2)
  159. full_path = '/'.join([m.module_bay.position for m in modules])
  160. name = name.replace(MODULE_TOKEN, full_path, 1)
  161. else:
  162. # Multiple tokens: substitute level-by-level (existing behavior)
  163. for m in modules:
  164. name = name.replace(MODULE_TOKEN, m.module_bay.position, 1)
  165. return name
  166. return self.name
  167. def resolve_label(self, module):
  168. if MODULE_TOKEN not in self.label:
  169. return self.label
  170. if module:
  171. modules = self._get_module_tree(module)
  172. token_count = self.label.count(MODULE_TOKEN)
  173. label = self.label
  174. if token_count == 1:
  175. # Single token: substitute with full path (e.g., "1/1" for depth 2)
  176. full_path = '/'.join([m.module_bay.position for m in modules])
  177. label = label.replace(MODULE_TOKEN, full_path, 1)
  178. else:
  179. # Multiple tokens: substitute level-by-level (existing behavior)
  180. for m in modules:
  181. label = label.replace(MODULE_TOKEN, m.module_bay.position, 1)
  182. return label
  183. return self.label
  184. def resolve_position(self, position, module):
  185. """
  186. Resolve {module} placeholder in position field.
  187. This is used by ModuleBayTemplate to resolve positions like "{module}/1"
  188. to actual values like "A/1" when the parent module is installed in bay "A".
  189. Fixes Issue #20467.
  190. """
  191. if not position or MODULE_TOKEN not in position:
  192. return position
  193. if module:
  194. modules = self._get_module_tree(module)
  195. token_count = position.count(MODULE_TOKEN)
  196. if token_count == 1:
  197. # Single token: substitute with full path
  198. full_path = '/'.join([m.module_bay.position for m in modules])
  199. position = position.replace(MODULE_TOKEN, full_path, 1)
  200. else:
  201. # Multiple tokens: substitute level-by-level
  202. for m in modules:
  203. position = position.replace(MODULE_TOKEN, m.module_bay.position, 1)
  204. return position
  205. return position
  206. class ConsolePortTemplate(ModularComponentTemplateModel):
  207. """
  208. A template for a ConsolePort to be created for a new Device.
  209. """
  210. type = models.CharField(
  211. verbose_name=_('type'),
  212. max_length=50,
  213. choices=ConsolePortTypeChoices,
  214. blank=True,
  215. null=True
  216. )
  217. component_model = ConsolePort
  218. class Meta(ModularComponentTemplateModel.Meta):
  219. verbose_name = _('console port template')
  220. verbose_name_plural = _('console port templates')
  221. def instantiate(self, **kwargs):
  222. return self.component_model(
  223. name=self.resolve_name(kwargs.get('module')),
  224. label=self.resolve_label(kwargs.get('module')),
  225. type=self.type,
  226. **kwargs
  227. )
  228. def to_yaml(self):
  229. return {
  230. 'name': self.name,
  231. 'type': self.type,
  232. 'label': self.label,
  233. 'description': self.description,
  234. }
  235. class ConsoleServerPortTemplate(ModularComponentTemplateModel):
  236. """
  237. A template for a ConsoleServerPort to be created for a new Device.
  238. """
  239. type = models.CharField(
  240. verbose_name=_('type'),
  241. max_length=50,
  242. choices=ConsolePortTypeChoices,
  243. blank=True,
  244. null=True
  245. )
  246. component_model = ConsoleServerPort
  247. class Meta(ModularComponentTemplateModel.Meta):
  248. verbose_name = _('console server port template')
  249. verbose_name_plural = _('console server port templates')
  250. def instantiate(self, **kwargs):
  251. return self.component_model(
  252. name=self.resolve_name(kwargs.get('module')),
  253. label=self.resolve_label(kwargs.get('module')),
  254. type=self.type,
  255. **kwargs
  256. )
  257. instantiate.do_not_call_in_templates = True
  258. def to_yaml(self):
  259. return {
  260. 'name': self.name,
  261. 'type': self.type,
  262. 'label': self.label,
  263. 'description': self.description,
  264. }
  265. class PowerPortTemplate(ModularComponentTemplateModel):
  266. """
  267. A template for a PowerPort to be created for a new Device.
  268. """
  269. type = models.CharField(
  270. verbose_name=_('type'),
  271. max_length=50,
  272. choices=PowerPortTypeChoices,
  273. blank=True,
  274. null=True
  275. )
  276. maximum_draw = models.PositiveIntegerField(
  277. verbose_name=_('maximum draw'),
  278. blank=True,
  279. null=True,
  280. validators=[MinValueValidator(1)],
  281. help_text=_('Maximum power draw (watts)')
  282. )
  283. allocated_draw = models.PositiveIntegerField(
  284. verbose_name=_('allocated draw'),
  285. blank=True,
  286. null=True,
  287. validators=[MinValueValidator(1)],
  288. help_text=_('Allocated power draw (watts)')
  289. )
  290. component_model = PowerPort
  291. class Meta(ModularComponentTemplateModel.Meta):
  292. verbose_name = _('power port template')
  293. verbose_name_plural = _('power port templates')
  294. def instantiate(self, **kwargs):
  295. return self.component_model(
  296. name=self.resolve_name(kwargs.get('module')),
  297. label=self.resolve_label(kwargs.get('module')),
  298. type=self.type,
  299. maximum_draw=self.maximum_draw,
  300. allocated_draw=self.allocated_draw,
  301. **kwargs
  302. )
  303. instantiate.do_not_call_in_templates = True
  304. def clean(self):
  305. super().clean()
  306. if self.maximum_draw is not None and self.allocated_draw is not None:
  307. if self.allocated_draw > self.maximum_draw:
  308. raise ValidationError({
  309. 'allocated_draw': _(
  310. "Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
  311. ).format(maximum_draw=self.maximum_draw)
  312. })
  313. def to_yaml(self):
  314. return {
  315. 'name': self.name,
  316. 'type': self.type,
  317. 'maximum_draw': self.maximum_draw,
  318. 'allocated_draw': self.allocated_draw,
  319. 'label': self.label,
  320. 'description': self.description,
  321. }
  322. class PowerOutletTemplate(ModularComponentTemplateModel):
  323. """
  324. A template for a PowerOutlet to be created for a new Device.
  325. """
  326. type = models.CharField(
  327. verbose_name=_('type'),
  328. max_length=50,
  329. choices=PowerOutletTypeChoices,
  330. blank=True,
  331. null=True
  332. )
  333. color = ColorField(
  334. verbose_name=_('color'),
  335. blank=True
  336. )
  337. power_port = models.ForeignKey(
  338. to='dcim.PowerPortTemplate',
  339. on_delete=models.SET_NULL,
  340. blank=True,
  341. null=True,
  342. related_name='poweroutlet_templates'
  343. )
  344. feed_leg = models.CharField(
  345. verbose_name=_('feed leg'),
  346. max_length=50,
  347. choices=PowerOutletFeedLegChoices,
  348. blank=True,
  349. null=True,
  350. help_text=_('Phase (for three-phase feeds)')
  351. )
  352. component_model = PowerOutlet
  353. class Meta(ModularComponentTemplateModel.Meta):
  354. verbose_name = _('power outlet template')
  355. verbose_name_plural = _('power outlet templates')
  356. def clean(self):
  357. super().clean()
  358. # Validate power port assignment
  359. if self.power_port:
  360. if self.device_type and self.power_port.device_type != self.device_type:
  361. raise ValidationError(
  362. _("Parent power port ({power_port}) must belong to the same device type").format(
  363. power_port=self.power_port
  364. )
  365. )
  366. if self.module_type and self.power_port.module_type != self.module_type:
  367. raise ValidationError(
  368. _("Parent power port ({power_port}) must belong to the same module type").format(
  369. power_port=self.power_port
  370. )
  371. )
  372. def instantiate(self, **kwargs):
  373. if self.power_port:
  374. power_port_name = self.power_port.resolve_name(kwargs.get('module'))
  375. power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
  376. else:
  377. power_port = None
  378. return self.component_model(
  379. name=self.resolve_name(kwargs.get('module')),
  380. label=self.resolve_label(kwargs.get('module')),
  381. type=self.type,
  382. color=self.color,
  383. power_port=power_port,
  384. feed_leg=self.feed_leg,
  385. **kwargs
  386. )
  387. instantiate.do_not_call_in_templates = True
  388. def to_yaml(self):
  389. return {
  390. 'name': self.name,
  391. 'type': self.type,
  392. 'color': self.color,
  393. 'power_port': self.power_port.name if self.power_port else None,
  394. 'feed_leg': self.feed_leg,
  395. 'label': self.label,
  396. 'description': self.description,
  397. }
  398. class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel):
  399. """
  400. A template for a physical data interface on a new Device.
  401. """
  402. # Override ComponentTemplateModel._name to specify naturalize_interface function
  403. _name = NaturalOrderingField(
  404. target_field='name',
  405. naturalize_function=naturalize_interface,
  406. max_length=100,
  407. blank=True
  408. )
  409. type = models.CharField(
  410. verbose_name=_('type'),
  411. max_length=50,
  412. choices=InterfaceTypeChoices
  413. )
  414. enabled = models.BooleanField(
  415. verbose_name=_('enabled'),
  416. default=True
  417. )
  418. mgmt_only = models.BooleanField(
  419. default=False,
  420. verbose_name=_('management only')
  421. )
  422. bridge = models.ForeignKey(
  423. to='self',
  424. on_delete=models.SET_NULL,
  425. related_name='bridge_interfaces',
  426. null=True,
  427. blank=True,
  428. verbose_name=_('bridge interface')
  429. )
  430. poe_mode = models.CharField(
  431. max_length=50,
  432. choices=InterfacePoEModeChoices,
  433. blank=True,
  434. null=True,
  435. verbose_name=_('PoE mode')
  436. )
  437. poe_type = models.CharField(
  438. max_length=50,
  439. choices=InterfacePoETypeChoices,
  440. blank=True,
  441. null=True,
  442. verbose_name=_('PoE type')
  443. )
  444. rf_role = models.CharField(
  445. max_length=30,
  446. choices=WirelessRoleChoices,
  447. blank=True,
  448. null=True,
  449. verbose_name=_('wireless role')
  450. )
  451. component_model = Interface
  452. class Meta(ModularComponentTemplateModel.Meta):
  453. verbose_name = _('interface template')
  454. verbose_name_plural = _('interface templates')
  455. def clean(self):
  456. super().clean()
  457. if self.bridge:
  458. if self.device_type and self.device_type != self.bridge.device_type:
  459. raise ValidationError({
  460. 'bridge': _(
  461. "Bridge interface ({bridge}) must belong to the same device type"
  462. ).format(bridge=self.bridge)
  463. })
  464. if self.module_type and self.module_type != self.bridge.module_type:
  465. raise ValidationError({
  466. 'bridge': _(
  467. "Bridge interface ({bridge}) must belong to the same module type"
  468. ).format(bridge=self.bridge)
  469. })
  470. def instantiate(self, **kwargs):
  471. return self.component_model(
  472. name=self.resolve_name(kwargs.get('module')),
  473. label=self.resolve_label(kwargs.get('module')),
  474. type=self.type,
  475. enabled=self.enabled,
  476. mgmt_only=self.mgmt_only,
  477. poe_mode=self.poe_mode,
  478. poe_type=self.poe_type,
  479. rf_role=self.rf_role,
  480. **kwargs
  481. )
  482. instantiate.do_not_call_in_templates = True
  483. def to_yaml(self):
  484. return {
  485. 'name': self.name,
  486. 'type': self.type,
  487. 'enabled': self.enabled,
  488. 'mgmt_only': self.mgmt_only,
  489. 'label': self.label,
  490. 'description': self.description,
  491. 'bridge': self.bridge.name if self.bridge else None,
  492. 'poe_mode': self.poe_mode,
  493. 'poe_type': self.poe_type,
  494. 'rf_role': self.rf_role,
  495. }
  496. class PortTemplateMapping(PortMappingBase):
  497. """
  498. Maps a FrontPortTemplate & position to a RearPortTemplate & position.
  499. """
  500. device_type = models.ForeignKey(
  501. to='dcim.DeviceType',
  502. on_delete=models.CASCADE,
  503. related_name='port_mappings',
  504. blank=True,
  505. null=True,
  506. )
  507. module_type = models.ForeignKey(
  508. to='dcim.ModuleType',
  509. on_delete=models.CASCADE,
  510. related_name='port_mappings',
  511. blank=True,
  512. null=True,
  513. )
  514. front_port = models.ForeignKey(
  515. to='dcim.FrontPortTemplate',
  516. on_delete=models.CASCADE,
  517. related_name='mappings',
  518. )
  519. rear_port = models.ForeignKey(
  520. to='dcim.RearPortTemplate',
  521. on_delete=models.CASCADE,
  522. related_name='mappings',
  523. )
  524. def clean(self):
  525. super().clean()
  526. # Validate rear port assignment
  527. if self.front_port.device_type_id != self.rear_port.device_type_id:
  528. raise ValidationError({
  529. "rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
  530. rear_port=self.rear_port
  531. )
  532. })
  533. def save(self, *args, **kwargs):
  534. # Associate the mapping with the parent DeviceType/ModuleType
  535. self.device_type = self.front_port.device_type
  536. self.module_type = self.front_port.module_type
  537. super().save(*args, **kwargs)
  538. class FrontPortTemplate(ModularComponentTemplateModel):
  539. """
  540. Template for a pass-through port on the front of a new Device.
  541. """
  542. type = models.CharField(
  543. verbose_name=_('type'),
  544. max_length=50,
  545. choices=PortTypeChoices
  546. )
  547. color = ColorField(
  548. verbose_name=_('color'),
  549. blank=True
  550. )
  551. positions = models.PositiveSmallIntegerField(
  552. verbose_name=_('positions'),
  553. default=1,
  554. validators=[
  555. MinValueValidator(PORT_POSITION_MIN),
  556. MaxValueValidator(PORT_POSITION_MAX)
  557. ],
  558. )
  559. component_model = FrontPort
  560. class Meta(ModularComponentTemplateModel.Meta):
  561. constraints = (
  562. models.UniqueConstraint(
  563. fields=('device_type', 'name'),
  564. name='%(app_label)s_%(class)s_unique_device_type_name'
  565. ),
  566. models.UniqueConstraint(
  567. fields=('module_type', 'name'),
  568. name='%(app_label)s_%(class)s_unique_module_type_name'
  569. ),
  570. )
  571. verbose_name = _('front port template')
  572. verbose_name_plural = _('front port templates')
  573. def clean(self):
  574. super().clean()
  575. # Check that positions is greater than or equal to the number of associated RearPortTemplates
  576. if not self._state.adding:
  577. mapping_count = self.mappings.count()
  578. if self.positions < mapping_count:
  579. raise ValidationError({
  580. "positions": _(
  581. "The number of positions cannot be less than the number of mapped rear port templates ({count})"
  582. ).format(count=mapping_count)
  583. })
  584. def instantiate(self, **kwargs):
  585. return self.component_model(
  586. name=self.resolve_name(kwargs.get('module')),
  587. label=self.resolve_label(kwargs.get('module')),
  588. type=self.type,
  589. color=self.color,
  590. positions=self.positions,
  591. **kwargs
  592. )
  593. instantiate.do_not_call_in_templates = True
  594. def to_yaml(self):
  595. return {
  596. 'name': self.name,
  597. 'type': self.type,
  598. 'color': self.color,
  599. 'positions': self.positions,
  600. 'label': self.label,
  601. 'description': self.description,
  602. }
  603. class RearPortTemplate(ModularComponentTemplateModel):
  604. """
  605. Template for a pass-through port on the rear of a new Device.
  606. """
  607. type = models.CharField(
  608. verbose_name=_('type'),
  609. max_length=50,
  610. choices=PortTypeChoices
  611. )
  612. color = ColorField(
  613. verbose_name=_('color'),
  614. blank=True
  615. )
  616. positions = models.PositiveSmallIntegerField(
  617. verbose_name=_('positions'),
  618. default=1,
  619. validators=[
  620. MinValueValidator(PORT_POSITION_MIN),
  621. MaxValueValidator(PORT_POSITION_MAX)
  622. ],
  623. )
  624. component_model = RearPort
  625. class Meta(ModularComponentTemplateModel.Meta):
  626. verbose_name = _('rear port template')
  627. verbose_name_plural = _('rear port templates')
  628. def clean(self):
  629. super().clean()
  630. # Check that positions is greater than or equal to the number of associated FrontPortTemplates
  631. if not self._state.adding:
  632. mapping_count = self.mappings.count()
  633. if self.positions < mapping_count:
  634. raise ValidationError({
  635. "positions": _(
  636. "The number of positions cannot be less than the number of mapped front port templates "
  637. "({count})"
  638. ).format(count=mapping_count)
  639. })
  640. def instantiate(self, **kwargs):
  641. return self.component_model(
  642. name=self.resolve_name(kwargs.get('module')),
  643. label=self.resolve_label(kwargs.get('module')),
  644. type=self.type,
  645. color=self.color,
  646. positions=self.positions,
  647. **kwargs
  648. )
  649. instantiate.do_not_call_in_templates = True
  650. def to_yaml(self):
  651. return {
  652. 'name': self.name,
  653. 'type': self.type,
  654. 'color': self.color,
  655. 'positions': self.positions,
  656. 'label': self.label,
  657. 'description': self.description,
  658. }
  659. class ModuleBayTemplate(ModularComponentTemplateModel):
  660. """
  661. A template for a ModuleBay to be created for a new parent Device.
  662. """
  663. position = models.CharField(
  664. verbose_name=_('position'),
  665. max_length=30,
  666. blank=True,
  667. help_text=_('Identifier to reference when renaming installed components')
  668. )
  669. component_model = ModuleBay
  670. class Meta(ModularComponentTemplateModel.Meta):
  671. verbose_name = _('module bay template')
  672. verbose_name_plural = _('module bay templates')
  673. def instantiate(self, **kwargs):
  674. module = kwargs.get('module')
  675. return self.component_model(
  676. name=self.resolve_name(module),
  677. label=self.resolve_label(module),
  678. position=self.resolve_position(self.position, module),
  679. **kwargs
  680. )
  681. instantiate.do_not_call_in_templates = True
  682. def to_yaml(self):
  683. return {
  684. 'name': self.name,
  685. 'label': self.label,
  686. 'position': self.position,
  687. 'description': self.description,
  688. }
  689. class DeviceBayTemplate(ComponentTemplateModel):
  690. """
  691. A template for a DeviceBay to be created for a new parent Device.
  692. """
  693. component_model = DeviceBay
  694. class Meta(ComponentTemplateModel.Meta):
  695. verbose_name = _('device bay template')
  696. verbose_name_plural = _('device bay templates')
  697. def instantiate(self, device):
  698. return self.component_model(
  699. device=device,
  700. name=self.name,
  701. label=self.label
  702. )
  703. instantiate.do_not_call_in_templates = True
  704. def clean(self):
  705. if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
  706. raise ValidationError(
  707. _(
  708. 'Subdevice role of device type ({device_type}) must be set to "parent" to allow device bays.'
  709. ).format(device_type=self.device_type)
  710. )
  711. def to_yaml(self):
  712. return {
  713. 'name': self.name,
  714. 'label': self.label,
  715. 'description': self.description,
  716. }
  717. class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
  718. """
  719. A template for an InventoryItem to be created for a new parent Device.
  720. """
  721. parent = TreeForeignKey(
  722. to='self',
  723. on_delete=models.CASCADE,
  724. related_name='child_items',
  725. blank=True,
  726. null=True,
  727. db_index=True
  728. )
  729. component_type = models.ForeignKey(
  730. to='contenttypes.ContentType',
  731. on_delete=models.PROTECT,
  732. related_name='+',
  733. blank=True,
  734. null=True
  735. )
  736. component_id = models.PositiveBigIntegerField(
  737. blank=True,
  738. null=True
  739. )
  740. component = GenericForeignKey(
  741. ct_field='component_type',
  742. fk_field='component_id'
  743. )
  744. role = models.ForeignKey(
  745. to='dcim.InventoryItemRole',
  746. on_delete=models.PROTECT,
  747. related_name='inventory_item_templates',
  748. blank=True,
  749. null=True
  750. )
  751. manufacturer = models.ForeignKey(
  752. to='dcim.Manufacturer',
  753. on_delete=models.PROTECT,
  754. related_name='inventory_item_templates',
  755. blank=True,
  756. null=True
  757. )
  758. part_id = models.CharField(
  759. max_length=50,
  760. verbose_name=_('part ID'),
  761. blank=True,
  762. help_text=_('Manufacturer-assigned part identifier')
  763. )
  764. objects = TreeManager()
  765. component_model = InventoryItem
  766. class Meta:
  767. ordering = ('device_type__id', 'parent__id', 'name')
  768. indexes = (
  769. models.Index(fields=('component_type', 'component_id')),
  770. )
  771. constraints = (
  772. models.UniqueConstraint(
  773. fields=('device_type', 'parent', 'name'),
  774. name='%(app_label)s_%(class)s_unique_device_type_parent_name'
  775. ),
  776. )
  777. verbose_name = _('inventory item template')
  778. verbose_name_plural = _('inventory item templates')
  779. def instantiate(self, **kwargs):
  780. parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None
  781. if self.component:
  782. model = self.component.component_model
  783. component = model.objects.get(name=self.component.name, **kwargs)
  784. else:
  785. component = None
  786. return self.component_model(
  787. parent=parent,
  788. name=self.name,
  789. label=self.label,
  790. component=component,
  791. role=self.role,
  792. manufacturer=self.manufacturer,
  793. part_id=self.part_id,
  794. **kwargs
  795. )
  796. instantiate.do_not_call_in_templates = True