models.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. from django.conf import settings
  2. from django.contrib.contenttypes.fields import GenericRelation
  3. from django.core.exceptions import ValidationError
  4. from django.db import models
  5. from django.urls import reverse
  6. from taggit.managers import TaggableManager
  7. from dcim.models import Device
  8. from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
  9. from utilities.models import ChangeLoggedModel
  10. from .constants import *
  11. #
  12. # Cluster types
  13. #
  14. class ClusterType(ChangeLoggedModel):
  15. """
  16. A type of Cluster.
  17. """
  18. name = models.CharField(
  19. max_length=50,
  20. unique=True
  21. )
  22. slug = models.SlugField(
  23. unique=True
  24. )
  25. csv_headers = ['name', 'slug']
  26. class Meta:
  27. ordering = ['name']
  28. def __str__(self):
  29. return self.name
  30. def get_absolute_url(self):
  31. return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
  32. def to_csv(self):
  33. return (
  34. self.name,
  35. self.slug,
  36. )
  37. #
  38. # Cluster groups
  39. #
  40. class ClusterGroup(ChangeLoggedModel):
  41. """
  42. An organizational group of Clusters.
  43. """
  44. name = models.CharField(
  45. max_length=50,
  46. unique=True
  47. )
  48. slug = models.SlugField(
  49. unique=True
  50. )
  51. csv_headers = ['name', 'slug']
  52. class Meta:
  53. ordering = ['name']
  54. def __str__(self):
  55. return self.name
  56. def get_absolute_url(self):
  57. return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
  58. def to_csv(self):
  59. return (
  60. self.name,
  61. self.slug,
  62. )
  63. #
  64. # Clusters
  65. #
  66. class Cluster(ChangeLoggedModel, CustomFieldModel):
  67. """
  68. A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
  69. """
  70. name = models.CharField(
  71. max_length=100,
  72. unique=True
  73. )
  74. type = models.ForeignKey(
  75. to=ClusterType,
  76. on_delete=models.PROTECT,
  77. related_name='clusters'
  78. )
  79. group = models.ForeignKey(
  80. to=ClusterGroup,
  81. on_delete=models.PROTECT,
  82. related_name='clusters',
  83. blank=True,
  84. null=True
  85. )
  86. site = models.ForeignKey(
  87. to='dcim.Site',
  88. on_delete=models.PROTECT,
  89. related_name='clusters',
  90. blank=True,
  91. null=True
  92. )
  93. comments = models.TextField(
  94. blank=True
  95. )
  96. custom_field_values = GenericRelation(
  97. to='extras.CustomFieldValue',
  98. content_type_field='obj_type',
  99. object_id_field='obj_id'
  100. )
  101. tags = TaggableManager(through=TaggedItem)
  102. csv_headers = ['name', 'type', 'group', 'site', 'comments']
  103. class Meta:
  104. ordering = ['name']
  105. def __str__(self):
  106. return self.name
  107. def get_absolute_url(self):
  108. return reverse('virtualization:cluster', args=[self.pk])
  109. def clean(self):
  110. # If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
  111. if self.pk and self.site:
  112. nonsite_devices = Device.objects.filter(cluster=self).exclude(site=self.site).count()
  113. if nonsite_devices:
  114. raise ValidationError({
  115. 'site': "{} devices are assigned as hosts for this cluster but are not in site {}".format(
  116. nonsite_devices, self.site
  117. )
  118. })
  119. def to_csv(self):
  120. return (
  121. self.name,
  122. self.type.name,
  123. self.group.name if self.group else None,
  124. self.site.name if self.site else None,
  125. self.comments,
  126. )
  127. #
  128. # Virtual machines
  129. #
  130. class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
  131. """
  132. A virtual machine which runs inside a Cluster.
  133. """
  134. cluster = models.ForeignKey(
  135. to='virtualization.Cluster',
  136. on_delete=models.PROTECT,
  137. related_name='virtual_machines'
  138. )
  139. tenant = models.ForeignKey(
  140. to='tenancy.Tenant',
  141. on_delete=models.PROTECT,
  142. related_name='virtual_machines',
  143. blank=True,
  144. null=True
  145. )
  146. platform = models.ForeignKey(
  147. to='dcim.Platform',
  148. on_delete=models.SET_NULL,
  149. related_name='virtual_machines',
  150. blank=True,
  151. null=True
  152. )
  153. name = models.CharField(
  154. max_length=64,
  155. unique=True
  156. )
  157. status = models.PositiveSmallIntegerField(
  158. choices=VM_STATUS_CHOICES,
  159. default=DEVICE_STATUS_ACTIVE,
  160. verbose_name='Status'
  161. )
  162. role = models.ForeignKey(
  163. to='dcim.DeviceRole',
  164. on_delete=models.PROTECT,
  165. related_name='virtual_machines',
  166. limit_choices_to={'vm_role': True},
  167. blank=True,
  168. null=True
  169. )
  170. primary_ip4 = models.OneToOneField(
  171. to='ipam.IPAddress',
  172. on_delete=models.SET_NULL,
  173. related_name='+',
  174. blank=True,
  175. null=True,
  176. verbose_name='Primary IPv4'
  177. )
  178. primary_ip6 = models.OneToOneField(
  179. to='ipam.IPAddress',
  180. on_delete=models.SET_NULL,
  181. related_name='+',
  182. blank=True,
  183. null=True,
  184. verbose_name='Primary IPv6'
  185. )
  186. vcpus = models.PositiveSmallIntegerField(
  187. blank=True,
  188. null=True,
  189. verbose_name='vCPUs'
  190. )
  191. memory = models.PositiveIntegerField(
  192. blank=True,
  193. null=True,
  194. verbose_name='Memory (MB)'
  195. )
  196. disk = models.PositiveIntegerField(
  197. blank=True,
  198. null=True,
  199. verbose_name='Disk (GB)'
  200. )
  201. comments = models.TextField(
  202. blank=True
  203. )
  204. custom_field_values = GenericRelation(
  205. to='extras.CustomFieldValue',
  206. content_type_field='obj_type',
  207. object_id_field='obj_id'
  208. )
  209. tags = TaggableManager(through=TaggedItem)
  210. csv_headers = [
  211. 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
  212. ]
  213. class Meta:
  214. ordering = ['name']
  215. def __str__(self):
  216. return self.name
  217. def get_absolute_url(self):
  218. return reverse('virtualization:virtualmachine', args=[self.pk])
  219. def clean(self):
  220. super().clean()
  221. # Validate primary IP addresses
  222. interfaces = self.interfaces.all()
  223. for field in ['primary_ip4', 'primary_ip6']:
  224. ip = getattr(self, field)
  225. if ip is not None:
  226. if ip.interface in interfaces:
  227. pass
  228. elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
  229. pass
  230. else:
  231. raise ValidationError({
  232. field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
  233. })
  234. def to_csv(self):
  235. return (
  236. self.name,
  237. self.get_status_display(),
  238. self.role.name if self.role else None,
  239. self.cluster.name,
  240. self.tenant.name if self.tenant else None,
  241. self.platform.name if self.platform else None,
  242. self.vcpus,
  243. self.memory,
  244. self.disk,
  245. self.comments,
  246. )
  247. def get_status_class(self):
  248. return VM_STATUS_CLASSES[self.status]
  249. @property
  250. def primary_ip(self):
  251. if settings.PREFER_IPV4 and self.primary_ip4:
  252. return self.primary_ip4
  253. elif self.primary_ip6:
  254. return self.primary_ip6
  255. elif self.primary_ip4:
  256. return self.primary_ip4
  257. else:
  258. return None
  259. @property
  260. def site(self):
  261. return self.cluster.site