models.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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 extras.utils import extras_features
  10. from utilities.models import ChangeLoggedModel
  11. from utilities.querysets import RestrictedQuerySet
  12. from .choices import *
  13. __all__ = (
  14. 'Cluster',
  15. 'ClusterGroup',
  16. 'ClusterType',
  17. 'VirtualMachine',
  18. )
  19. #
  20. # Cluster types
  21. #
  22. class ClusterType(ChangeLoggedModel):
  23. """
  24. A type of Cluster.
  25. """
  26. name = models.CharField(
  27. max_length=50,
  28. unique=True
  29. )
  30. slug = models.SlugField(
  31. unique=True
  32. )
  33. description = models.CharField(
  34. max_length=200,
  35. blank=True
  36. )
  37. objects = RestrictedQuerySet.as_manager()
  38. csv_headers = ['name', 'slug', 'description']
  39. class Meta:
  40. ordering = ['name']
  41. def __str__(self):
  42. return self.name
  43. def get_absolute_url(self):
  44. return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
  45. def to_csv(self):
  46. return (
  47. self.name,
  48. self.slug,
  49. self.description,
  50. )
  51. #
  52. # Cluster groups
  53. #
  54. class ClusterGroup(ChangeLoggedModel):
  55. """
  56. An organizational group of Clusters.
  57. """
  58. name = models.CharField(
  59. max_length=50,
  60. unique=True
  61. )
  62. slug = models.SlugField(
  63. unique=True
  64. )
  65. description = models.CharField(
  66. max_length=200,
  67. blank=True
  68. )
  69. objects = RestrictedQuerySet.as_manager()
  70. csv_headers = ['name', 'slug', 'description']
  71. class Meta:
  72. ordering = ['name']
  73. def __str__(self):
  74. return self.name
  75. def get_absolute_url(self):
  76. return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
  77. def to_csv(self):
  78. return (
  79. self.name,
  80. self.slug,
  81. self.description,
  82. )
  83. #
  84. # Clusters
  85. #
  86. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  87. class Cluster(ChangeLoggedModel, CustomFieldModel):
  88. """
  89. A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
  90. """
  91. name = models.CharField(
  92. max_length=100,
  93. unique=True
  94. )
  95. type = models.ForeignKey(
  96. to=ClusterType,
  97. on_delete=models.PROTECT,
  98. related_name='clusters'
  99. )
  100. group = models.ForeignKey(
  101. to=ClusterGroup,
  102. on_delete=models.PROTECT,
  103. related_name='clusters',
  104. blank=True,
  105. null=True
  106. )
  107. tenant = models.ForeignKey(
  108. to='tenancy.Tenant',
  109. on_delete=models.PROTECT,
  110. related_name='clusters',
  111. blank=True,
  112. null=True
  113. )
  114. site = models.ForeignKey(
  115. to='dcim.Site',
  116. on_delete=models.PROTECT,
  117. related_name='clusters',
  118. blank=True,
  119. null=True
  120. )
  121. comments = models.TextField(
  122. blank=True
  123. )
  124. custom_field_values = GenericRelation(
  125. to='extras.CustomFieldValue',
  126. content_type_field='obj_type',
  127. object_id_field='obj_id'
  128. )
  129. tags = TaggableManager(through=TaggedItem)
  130. objects = RestrictedQuerySet.as_manager()
  131. csv_headers = ['name', 'type', 'group', 'site', 'comments']
  132. clone_fields = [
  133. 'type', 'group', 'tenant', 'site',
  134. ]
  135. class Meta:
  136. ordering = ['name']
  137. def __str__(self):
  138. return self.name
  139. def get_absolute_url(self):
  140. return reverse('virtualization:cluster', args=[self.pk])
  141. def clean(self):
  142. # If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
  143. if self.pk and self.site:
  144. nonsite_devices = Device.objects.filter(cluster=self).exclude(site=self.site).count()
  145. if nonsite_devices:
  146. raise ValidationError({
  147. 'site': "{} devices are assigned as hosts for this cluster but are not in site {}".format(
  148. nonsite_devices, self.site
  149. )
  150. })
  151. def to_csv(self):
  152. return (
  153. self.name,
  154. self.type.name,
  155. self.group.name if self.group else None,
  156. self.site.name if self.site else None,
  157. self.tenant.name if self.tenant else None,
  158. self.comments,
  159. )
  160. #
  161. # Virtual machines
  162. #
  163. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  164. class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
  165. """
  166. A virtual machine which runs inside a Cluster.
  167. """
  168. cluster = models.ForeignKey(
  169. to='virtualization.Cluster',
  170. on_delete=models.PROTECT,
  171. related_name='virtual_machines'
  172. )
  173. tenant = models.ForeignKey(
  174. to='tenancy.Tenant',
  175. on_delete=models.PROTECT,
  176. related_name='virtual_machines',
  177. blank=True,
  178. null=True
  179. )
  180. platform = models.ForeignKey(
  181. to='dcim.Platform',
  182. on_delete=models.SET_NULL,
  183. related_name='virtual_machines',
  184. blank=True,
  185. null=True
  186. )
  187. name = models.CharField(
  188. max_length=64
  189. )
  190. status = models.CharField(
  191. max_length=50,
  192. choices=VirtualMachineStatusChoices,
  193. default=VirtualMachineStatusChoices.STATUS_ACTIVE,
  194. verbose_name='Status'
  195. )
  196. role = models.ForeignKey(
  197. to='dcim.DeviceRole',
  198. on_delete=models.PROTECT,
  199. related_name='virtual_machines',
  200. limit_choices_to={'vm_role': True},
  201. blank=True,
  202. null=True
  203. )
  204. primary_ip4 = models.OneToOneField(
  205. to='ipam.IPAddress',
  206. on_delete=models.SET_NULL,
  207. related_name='+',
  208. blank=True,
  209. null=True,
  210. verbose_name='Primary IPv4'
  211. )
  212. primary_ip6 = models.OneToOneField(
  213. to='ipam.IPAddress',
  214. on_delete=models.SET_NULL,
  215. related_name='+',
  216. blank=True,
  217. null=True,
  218. verbose_name='Primary IPv6'
  219. )
  220. vcpus = models.PositiveSmallIntegerField(
  221. blank=True,
  222. null=True,
  223. verbose_name='vCPUs'
  224. )
  225. memory = models.PositiveIntegerField(
  226. blank=True,
  227. null=True,
  228. verbose_name='Memory (MB)'
  229. )
  230. disk = models.PositiveIntegerField(
  231. blank=True,
  232. null=True,
  233. verbose_name='Disk (GB)'
  234. )
  235. comments = models.TextField(
  236. blank=True
  237. )
  238. custom_field_values = GenericRelation(
  239. to='extras.CustomFieldValue',
  240. content_type_field='obj_type',
  241. object_id_field='obj_id'
  242. )
  243. tags = TaggableManager(through=TaggedItem)
  244. objects = RestrictedQuerySet.as_manager()
  245. csv_headers = [
  246. 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
  247. ]
  248. clone_fields = [
  249. 'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
  250. ]
  251. STATUS_CLASS_MAP = {
  252. VirtualMachineStatusChoices.STATUS_OFFLINE: 'warning',
  253. VirtualMachineStatusChoices.STATUS_ACTIVE: 'success',
  254. VirtualMachineStatusChoices.STATUS_PLANNED: 'info',
  255. VirtualMachineStatusChoices.STATUS_STAGED: 'primary',
  256. VirtualMachineStatusChoices.STATUS_FAILED: 'danger',
  257. VirtualMachineStatusChoices.STATUS_DECOMMISSIONING: 'warning',
  258. }
  259. class Meta:
  260. ordering = ('name', 'pk') # Name may be non-unique
  261. unique_together = [
  262. ['cluster', 'tenant', 'name']
  263. ]
  264. def __str__(self):
  265. return self.name
  266. def get_absolute_url(self):
  267. return reverse('virtualization:virtualmachine', args=[self.pk])
  268. def validate_unique(self, exclude=None):
  269. # Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary
  270. # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
  271. # of the uniqueness constraint without manual intervention.
  272. if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter(
  273. name=self.name, tenant__isnull=True
  274. ):
  275. raise ValidationError({
  276. 'name': 'A virtual machine with this name already exists.'
  277. })
  278. super().validate_unique(exclude)
  279. def clean(self):
  280. super().clean()
  281. # Validate primary IP addresses
  282. interfaces = self.interfaces.all()
  283. for field in ['primary_ip4', 'primary_ip6']:
  284. ip = getattr(self, field)
  285. if ip is not None:
  286. if ip.interface in interfaces:
  287. pass
  288. elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
  289. pass
  290. else:
  291. raise ValidationError({
  292. field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
  293. })
  294. def to_csv(self):
  295. return (
  296. self.name,
  297. self.get_status_display(),
  298. self.role.name if self.role else None,
  299. self.cluster.name,
  300. self.tenant.name if self.tenant else None,
  301. self.platform.name if self.platform else None,
  302. self.vcpus,
  303. self.memory,
  304. self.disk,
  305. self.comments,
  306. )
  307. def get_status_class(self):
  308. return self.STATUS_CLASS_MAP.get(self.status)
  309. @property
  310. def primary_ip(self):
  311. if settings.PREFER_IPV4 and self.primary_ip4:
  312. return self.primary_ip4
  313. elif self.primary_ip6:
  314. return self.primary_ip6
  315. elif self.primary_ip4:
  316. return self.primary_ip4
  317. else:
  318. return None
  319. @property
  320. def site(self):
  321. return self.cluster.site