| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- from django.conf import settings
- from django.contrib.contenttypes.fields import GenericRelation
- from django.core.exceptions import ValidationError
- from django.db import models
- from django.urls import reverse
- from taggit.managers import TaggableManager
- from dcim.models import BaseInterface, Device
- from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
- from extras.querysets import ConfigContextModelQuerySet
- from extras.utils import extras_features
- from utilities.fields import NaturalOrderingField
- from utilities.ordering import naturalize_interface
- from utilities.query_functions import CollateAsChar
- from utilities.querysets import RestrictedQuerySet
- from utilities.utils import serialize_object
- from .choices import *
- __all__ = (
- 'Cluster',
- 'ClusterGroup',
- 'ClusterType',
- 'VirtualMachine',
- 'VMInterface',
- )
- #
- # Cluster types
- #
- class ClusterType(ChangeLoggedModel):
- """
- A type of Cluster.
- """
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- objects = RestrictedQuerySet.as_manager()
- csv_headers = ['name', 'slug', 'description']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.description,
- )
- #
- # Cluster groups
- #
- class ClusterGroup(ChangeLoggedModel):
- """
- An organizational group of Clusters.
- """
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- objects = RestrictedQuerySet.as_manager()
- csv_headers = ['name', 'slug', 'description']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.description,
- )
- #
- # Clusters
- #
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
- class Cluster(ChangeLoggedModel, CustomFieldModel):
- """
- A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
- """
- name = models.CharField(
- max_length=100,
- unique=True
- )
- type = models.ForeignKey(
- to=ClusterType,
- on_delete=models.PROTECT,
- related_name='clusters'
- )
- group = models.ForeignKey(
- to=ClusterGroup,
- on_delete=models.PROTECT,
- related_name='clusters',
- blank=True,
- null=True
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='clusters',
- blank=True,
- null=True
- )
- site = models.ForeignKey(
- to='dcim.Site',
- on_delete=models.PROTECT,
- related_name='clusters',
- blank=True,
- null=True
- )
- comments = models.TextField(
- blank=True
- )
- tags = TaggableManager(through=TaggedItem)
- objects = RestrictedQuerySet.as_manager()
- csv_headers = ['name', 'type', 'group', 'site', 'comments']
- clone_fields = [
- 'type', 'group', 'tenant', 'site',
- ]
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('virtualization:cluster', args=[self.pk])
- def clean(self):
- super().clean()
- # If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
- if self.pk and self.site:
- nonsite_devices = Device.objects.filter(cluster=self).exclude(site=self.site).count()
- if nonsite_devices:
- raise ValidationError({
- 'site': "{} devices are assigned as hosts for this cluster but are not in site {}".format(
- nonsite_devices, self.site
- )
- })
- def to_csv(self):
- return (
- self.name,
- self.type.name,
- self.group.name if self.group else None,
- self.site.name if self.site else None,
- self.tenant.name if self.tenant else None,
- self.comments,
- )
- #
- # Virtual machines
- #
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
- class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
- """
- A virtual machine which runs inside a Cluster.
- """
- cluster = models.ForeignKey(
- to='virtualization.Cluster',
- on_delete=models.PROTECT,
- related_name='virtual_machines'
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='virtual_machines',
- blank=True,
- null=True
- )
- platform = models.ForeignKey(
- to='dcim.Platform',
- on_delete=models.SET_NULL,
- related_name='virtual_machines',
- blank=True,
- null=True
- )
- name = models.CharField(
- max_length=64
- )
- status = models.CharField(
- max_length=50,
- choices=VirtualMachineStatusChoices,
- default=VirtualMachineStatusChoices.STATUS_ACTIVE,
- verbose_name='Status'
- )
- role = models.ForeignKey(
- to='dcim.DeviceRole',
- on_delete=models.PROTECT,
- related_name='virtual_machines',
- limit_choices_to={'vm_role': True},
- blank=True,
- null=True
- )
- primary_ip4 = models.OneToOneField(
- to='ipam.IPAddress',
- on_delete=models.SET_NULL,
- related_name='+',
- blank=True,
- null=True,
- verbose_name='Primary IPv4'
- )
- primary_ip6 = models.OneToOneField(
- to='ipam.IPAddress',
- on_delete=models.SET_NULL,
- related_name='+',
- blank=True,
- null=True,
- verbose_name='Primary IPv6'
- )
- vcpus = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- verbose_name='vCPUs'
- )
- memory = models.PositiveIntegerField(
- blank=True,
- null=True,
- verbose_name='Memory (MB)'
- )
- disk = models.PositiveIntegerField(
- blank=True,
- null=True,
- verbose_name='Disk (GB)'
- )
- comments = models.TextField(
- blank=True
- )
- secrets = GenericRelation(
- to='secrets.Secret',
- content_type_field='assigned_object_type',
- object_id_field='assigned_object_id',
- related_query_name='virtual_machine'
- )
- tags = TaggableManager(through=TaggedItem)
- objects = ConfigContextModelQuerySet.as_manager()
- csv_headers = [
- 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
- ]
- clone_fields = [
- 'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
- ]
- class Meta:
- ordering = ('name', 'pk') # Name may be non-unique
- unique_together = [
- ['cluster', 'tenant', 'name']
- ]
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('virtualization:virtualmachine', args=[self.pk])
- def validate_unique(self, exclude=None):
- # Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary
- # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
- # of the uniqueness constraint without manual intervention.
- if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter(
- name=self.name, cluster=self.cluster, tenant__isnull=True
- ):
- raise ValidationError({
- 'name': 'A virtual machine with this name already exists in the assigned cluster.'
- })
- super().validate_unique(exclude)
- def clean(self):
- super().clean()
- # Validate primary IP addresses
- interfaces = self.interfaces.all()
- for field in ['primary_ip4', 'primary_ip6']:
- ip = getattr(self, field)
- if ip is not None:
- if ip.assigned_object in interfaces:
- pass
- elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces:
- pass
- else:
- raise ValidationError({
- field: f"The specified IP address ({ip}) is not assigned to this VM.",
- })
- def to_csv(self):
- return (
- self.name,
- self.get_status_display(),
- self.role.name if self.role else None,
- self.cluster.name,
- self.tenant.name if self.tenant else None,
- self.platform.name if self.platform else None,
- self.vcpus,
- self.memory,
- self.disk,
- self.comments,
- )
- def get_status_class(self):
- return VirtualMachineStatusChoices.CSS_CLASSES.get(self.status)
- @property
- def primary_ip(self):
- if settings.PREFER_IPV4 and self.primary_ip4:
- return self.primary_ip4
- elif self.primary_ip6:
- return self.primary_ip6
- elif self.primary_ip4:
- return self.primary_ip4
- else:
- return None
- @property
- def site(self):
- return self.cluster.site
- #
- # Interfaces
- #
- @extras_features('export_templates', 'webhooks')
- class VMInterface(BaseInterface):
- virtual_machine = models.ForeignKey(
- to='virtualization.VirtualMachine',
- on_delete=models.CASCADE,
- related_name='interfaces'
- )
- name = models.CharField(
- max_length=64
- )
- _name = NaturalOrderingField(
- target_field='name',
- naturalize_function=naturalize_interface,
- max_length=100,
- blank=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- untagged_vlan = models.ForeignKey(
- to='ipam.VLAN',
- on_delete=models.SET_NULL,
- related_name='vminterfaces_as_untagged',
- null=True,
- blank=True,
- verbose_name='Untagged VLAN'
- )
- tagged_vlans = models.ManyToManyField(
- to='ipam.VLAN',
- related_name='vminterfaces_as_tagged',
- blank=True,
- verbose_name='Tagged VLANs'
- )
- ip_addresses = GenericRelation(
- to='ipam.IPAddress',
- content_type_field='assigned_object_type',
- object_id_field='assigned_object_id',
- related_query_name='vminterface'
- )
- tags = TaggableManager(
- through=TaggedItem,
- related_name='vminterface'
- )
- objects = RestrictedQuerySet.as_manager()
- csv_headers = [
- 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
- ]
- class Meta:
- verbose_name = 'interface'
- ordering = ('virtual_machine', CollateAsChar('_name'))
- unique_together = ('virtual_machine', 'name')
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('virtualization:vminterface', kwargs={'pk': self.pk})
- def to_csv(self):
- return (
- self.virtual_machine.name,
- self.name,
- self.enabled,
- self.mac_address,
- self.mtu,
- self.description,
- self.get_mode_display(),
- )
- def clean(self):
- # Validate untagged VLAN
- if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
- raise ValidationError({
- 'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
- f"interface's parent virtual machine, or it must be global"
- })
- def to_objectchange(self, action):
- # Annotate the parent VirtualMachine
- return ObjectChange(
- changed_object=self,
- object_repr=str(self),
- action=action,
- related_object=self.virtual_machine,
- object_data=serialize_object(self)
- )
- @property
- def parent(self):
- return self.virtual_machine
- @property
- def count_ipaddresses(self):
- return self.ip_addresses.count()
|