mixins.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. from django.apps import apps
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.core.exceptions import ValidationError
  4. from django.db import models
  5. from django.utils.translation import gettext_lazy as _
  6. from dcim.constants import VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES
  7. __all__ = (
  8. 'CachedScopeMixin',
  9. 'InterfaceValidationMixin',
  10. 'RenderConfigMixin',
  11. )
  12. class RenderConfigMixin(models.Model):
  13. config_template = models.ForeignKey(
  14. to='extras.ConfigTemplate',
  15. on_delete=models.PROTECT,
  16. related_name='%(class)ss',
  17. blank=True,
  18. null=True
  19. )
  20. class Meta:
  21. abstract = True
  22. def get_config_template(self):
  23. """
  24. Return the appropriate ConfigTemplate (if any) for this Device.
  25. """
  26. if self.config_template:
  27. return self.config_template
  28. if self.role and self.role.config_template:
  29. return self.role.config_template
  30. if self.platform and self.platform.config_template:
  31. return self.platform.config_template
  32. return None
  33. class CachedScopeMixin(models.Model):
  34. """
  35. Mixin for adding a GenericForeignKey scope to a model that can point to a Region, SiteGroup, Site, or Location.
  36. Includes cached fields for each to allow efficient filtering. Appropriate validation must be done in the clean()
  37. method as this does not have any as validation is generally model-specific.
  38. """
  39. scope_type = models.ForeignKey(
  40. to='contenttypes.ContentType',
  41. on_delete=models.PROTECT,
  42. related_name='+',
  43. blank=True,
  44. null=True
  45. )
  46. scope_id = models.PositiveBigIntegerField(
  47. blank=True,
  48. null=True
  49. )
  50. scope = GenericForeignKey(
  51. ct_field='scope_type',
  52. fk_field='scope_id'
  53. )
  54. _location = models.ForeignKey(
  55. to='dcim.Location',
  56. on_delete=models.CASCADE,
  57. blank=True,
  58. null=True
  59. )
  60. _site = models.ForeignKey(
  61. to='dcim.Site',
  62. on_delete=models.CASCADE,
  63. blank=True,
  64. null=True
  65. )
  66. _region = models.ForeignKey(
  67. to='dcim.Region',
  68. on_delete=models.CASCADE,
  69. blank=True,
  70. null=True
  71. )
  72. _site_group = models.ForeignKey(
  73. to='dcim.SiteGroup',
  74. on_delete=models.CASCADE,
  75. blank=True,
  76. null=True
  77. )
  78. class Meta:
  79. abstract = True
  80. def clean(self):
  81. if self.scope_type and not (self.scope or self.scope_id):
  82. scope_type = self.scope_type.model_class()
  83. raise ValidationError(
  84. _("Please select a {scope_type}.").format(scope_type=scope_type._meta.model_name)
  85. )
  86. super().clean()
  87. def save(self, *args, **kwargs):
  88. # Cache objects associated with the terminating object (for filtering)
  89. self.cache_related_objects()
  90. super().save(*args, **kwargs)
  91. def cache_related_objects(self):
  92. self._region = self._site_group = self._site = self._location = None
  93. if self.scope_type:
  94. scope_type = self.scope_type.model_class()
  95. if scope_type == apps.get_model('dcim', 'region'):
  96. self._region = self.scope
  97. elif scope_type == apps.get_model('dcim', 'sitegroup'):
  98. self._site_group = self.scope
  99. elif scope_type == apps.get_model('dcim', 'site'):
  100. self._region = self.scope.region
  101. self._site_group = self.scope.group
  102. self._site = self.scope
  103. elif scope_type == apps.get_model('dcim', 'location'):
  104. self._region = self.scope.site.region
  105. self._site_group = self.scope.site.group
  106. self._site = self.scope.site
  107. self._location = self.scope
  108. cache_related_objects.alters_data = True
  109. class InterfaceValidationMixin:
  110. def clean(self):
  111. super().clean()
  112. # An interface cannot be bridged to itself
  113. if self.pk and self.bridge_id == self.pk:
  114. raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
  115. # Only physical interfaces may have a PoE mode/type assigned
  116. if self.poe_mode and self.type in VIRTUAL_IFACE_TYPES:
  117. raise ValidationError({
  118. 'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
  119. })
  120. if self.poe_type and self.type in VIRTUAL_IFACE_TYPES:
  121. raise ValidationError({
  122. 'poe_type': _("Virtual interfaces cannot have a PoE type.")
  123. })
  124. # An interface with a PoE type set must also specify a mode
  125. if self.poe_type and not self.poe_mode:
  126. raise ValidationError({
  127. 'poe_type': _("Must specify PoE mode when designating a PoE type.")
  128. })
  129. # RF role may be set only for wireless interfaces
  130. if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
  131. raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})