configcontexts.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. from collections import OrderedDict
  2. from django.core.validators import ValidationError
  3. from django.db import models
  4. from django.urls import reverse
  5. from extras.querysets import ConfigContextQuerySet
  6. from netbox.models import ChangeLoggedModel
  7. from netbox.models.features import WebhooksMixin
  8. from utilities.utils import deepmerge
  9. __all__ = (
  10. 'ConfigContext',
  11. 'ConfigContextModel',
  12. )
  13. #
  14. # Config contexts
  15. #
  16. class ConfigContext(WebhooksMixin, ChangeLoggedModel):
  17. """
  18. A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
  19. qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
  20. will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
  21. """
  22. name = models.CharField(
  23. max_length=100,
  24. unique=True
  25. )
  26. weight = models.PositiveSmallIntegerField(
  27. default=1000
  28. )
  29. description = models.CharField(
  30. max_length=200,
  31. blank=True
  32. )
  33. is_active = models.BooleanField(
  34. default=True,
  35. )
  36. regions = models.ManyToManyField(
  37. to='dcim.Region',
  38. related_name='+',
  39. blank=True
  40. )
  41. site_groups = models.ManyToManyField(
  42. to='dcim.SiteGroup',
  43. related_name='+',
  44. blank=True
  45. )
  46. sites = models.ManyToManyField(
  47. to='dcim.Site',
  48. related_name='+',
  49. blank=True
  50. )
  51. device_types = models.ManyToManyField(
  52. to='dcim.DeviceType',
  53. related_name='+',
  54. blank=True
  55. )
  56. roles = models.ManyToManyField(
  57. to='dcim.DeviceRole',
  58. related_name='+',
  59. blank=True
  60. )
  61. platforms = models.ManyToManyField(
  62. to='dcim.Platform',
  63. related_name='+',
  64. blank=True
  65. )
  66. cluster_types = models.ManyToManyField(
  67. to='virtualization.ClusterType',
  68. related_name='+',
  69. blank=True
  70. )
  71. cluster_groups = models.ManyToManyField(
  72. to='virtualization.ClusterGroup',
  73. related_name='+',
  74. blank=True
  75. )
  76. clusters = models.ManyToManyField(
  77. to='virtualization.Cluster',
  78. related_name='+',
  79. blank=True
  80. )
  81. tenant_groups = models.ManyToManyField(
  82. to='tenancy.TenantGroup',
  83. related_name='+',
  84. blank=True
  85. )
  86. tenants = models.ManyToManyField(
  87. to='tenancy.Tenant',
  88. related_name='+',
  89. blank=True
  90. )
  91. tags = models.ManyToManyField(
  92. to='extras.Tag',
  93. related_name='+',
  94. blank=True
  95. )
  96. data = models.JSONField()
  97. objects = ConfigContextQuerySet.as_manager()
  98. class Meta:
  99. ordering = ['weight', 'name']
  100. def __str__(self):
  101. return self.name
  102. def get_absolute_url(self):
  103. return reverse('extras:configcontext', kwargs={'pk': self.pk})
  104. def clean(self):
  105. super().clean()
  106. # Verify that JSON data is provided as an object
  107. if type(self.data) is not dict:
  108. raise ValidationError(
  109. {'data': 'JSON data must be in object form. Example: {"foo": 123}'}
  110. )
  111. class ConfigContextModel(models.Model):
  112. """
  113. A model which includes local configuration context data. This local data will override any inherited data from
  114. ConfigContexts.
  115. """
  116. local_context_data = models.JSONField(
  117. blank=True,
  118. null=True,
  119. )
  120. class Meta:
  121. abstract = True
  122. def get_config_context(self):
  123. """
  124. Return the rendered configuration context for a device or VM.
  125. """
  126. # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
  127. data = OrderedDict()
  128. if not hasattr(self, 'config_context_data'):
  129. # The annotation is not available, so we fall back to manually querying for the config context objects
  130. config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
  131. else:
  132. # The attribute may exist, but the annotated value could be None if there is no config context data
  133. config_context_data = self.config_context_data or []
  134. for context in config_context_data:
  135. data = deepmerge(data, context)
  136. # If the object has local config context data defined, merge it last
  137. if self.local_context_data:
  138. data = deepmerge(data, self.local_context_data)
  139. return data
  140. def clean(self):
  141. super().clean()
  142. # Verify that JSON data is provided as an object
  143. if self.local_context_data and type(self.local_context_data) is not dict:
  144. raise ValidationError(
  145. {'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
  146. )