import uuid from django.db import models from django.contrib.auth import get_user_model from uncloud_pay.models import Product, RecurringPeriod from uncloud.models import UncloudModel, UncloudStatus import uncloud_pay.models as pay_models import uncloud_storage.models class VMCluster(UncloudModel): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=128, unique=True) class VMHost(UncloudModel): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # 253 is the maximum DNS name length hostname = models.CharField(max_length=253, unique=True) vmcluster = models.ForeignKey( VMCluster, on_delete=models.CASCADE, editable=False, blank=True, null=True ) # indirectly gives a maximum number of cores / VM - f.i. 32 physical_cores = models.IntegerField(default=0) # determines the maximum usable cores - f.i. 320 if you overbook by a factor of 10 usable_cores = models.IntegerField(default=0) # ram that can be used of the server usable_ram_in_gb = models.FloatField(default=0) status = models.CharField( max_length=32, choices=UncloudStatus.choices, default=UncloudStatus.PENDING ) @property def vms(self): return VMProduct.objects.filter(vmhost=self) @property def used_ram_in_gb(self): return sum([vm.ram_in_gb for vm in VMProduct.objects.filter(vmhost=self)]) @property def available_ram_in_gb(self): return self.usable_ram_in_gb - self.used_ram_in_gb @property def available_cores(self): return self.usable_cores - sum([vm.cores for vm in self.vms ]) class VMProduct(Product): vmhost = models.ForeignKey( VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True ) vmcluster = models.ForeignKey( VMCluster, on_delete=models.CASCADE, editable=False, blank=True, null=True ) # VM-specific. The name is only intended for customers: it's a pain to # remember IDs (speaking from experience as ungleich customer)! name = models.CharField(max_length=32, blank=True, null=True) cores = models.IntegerField() ram_in_gb = models.FloatField() # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. @property def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 def __str__(self): if self.name: name = f"{self.name} ({self.id})" else: name = self.id return "VM {}: {} cores {} gb ram".format(name, self.cores, self.ram_in_gb) @property def description(self): return "Virtual machine '{}': {} core(s), {}GB memory".format( self.name, self.cores, self.ram_in_gb) @staticmethod def allowed_recurring_periods(): return list(filter( lambda pair: pair[0] in [RecurringPeriod.PER_365D, RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR], RecurringPeriod.choices)) def __str__(self): return "VM {} ({} Cores/{} GB RAM) running on {} in cluster {}".format( self.uuid, self.cores, self.ram_in_gb, self.vmhost, self.vmcluster) class VMWithOSProduct(VMProduct): primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) class VMDiskImageProduct(UncloudModel): """ Images are used for cloning/linking. They are the base for images. """ uuid = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) owner = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, editable=False ) name = models.CharField(max_length=256) is_os_image = models.BooleanField(default=False) is_public = models.BooleanField(default=False, editable=False) # only allow admins to set this size_in_gb = models.FloatField(null=True, blank=True) import_url = models.URLField(null=True, blank=True) image_source = models.CharField(max_length=128, null=True) image_source_type = models.CharField(max_length=128, null=True) storage_class = models.CharField(max_length=32, choices = uncloud_storage.models.StorageClass.choices, default = uncloud_storage.models.StorageClass.SSD) status = models.CharField( max_length=32, choices=UncloudStatus.choices, default=UncloudStatus.PENDING ) def __str__(self): return "VMDiskImage {} ({}): {} gb".format(self.uuid, self.name, self.size_in_gb) # See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types class VMDiskType(models.TextChoices): """ Types of disks that can be attached to VMs """ CEPH_SSD = 'ceph/ssd' CEPH_HDD = 'ceph/hdd' LOCAL_SSD = 'local/ssd' LOCAL_HDD = 'local/hdd' class VMDiskProduct(Product): """ The VMDiskProduct is attached to a VM. It is based on a VMDiskImageProduct that will be used as a basis. It can be enlarged, but not shrinked compared to the VMDiskImageProduct. """ vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) image = models.ForeignKey(VMDiskImageProduct, on_delete=models.CASCADE) size_in_gb = models.FloatField(blank=True) disk_type = models.CharField( max_length=20, choices=VMDiskType.choices, default=VMDiskType.CEPH_SSD) @property def description(self): return "Disk for VM '{}': {}GB".format(self.vm.name, self.size_in_gb) @property def recurring_price(self): return (self.size_in_gb / 10) * 3.5 # Ensures that a VMDiskProduct can only be created from a VMDiskImageProduct # that is in status 'active' # This might not be need in "for billing only" constellations # def clean(self): # if self.image.status != 'active': # raise ValidationError({ # 'image': 'VM Disk must be created from an active disk image.' # }) def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs) class VMNetworkCard(models.Model): vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) mac_address = models.BigIntegerField() ip_address = models.GenericIPAddressField(blank=True, null=True) class VMSnapshotProduct(Product): gb_ssd = models.FloatField(editable=False) gb_hdd = models.FloatField(editable=False) vm = models.ForeignKey(VMProduct, related_name='snapshots', on_delete=models.CASCADE)