diff --git a/uncloud/uncloud_vm/models.py b/uncloud/uncloud_vm/models.py index 7aac05b..9733841 100644 --- a/uncloud/uncloud_vm/models.py +++ b/uncloud/uncloud_vm/models.py @@ -1,19 +1,24 @@ +import uuid + from django.db import models from django.contrib.auth import get_user_model -import uuid + +# Uncomment if you override model's clean method +# from django.core.exceptions import ValidationError from uncloud_pay.models import Product STATUS_CHOICES = ( ('pending', 'Pending'), # Initial state - ('creating', 'Creating'), # Creating VM/image/etc. - ('active', 'Active'), # Is usable / active - ('disabled', 'Disabled'), # Is usable, but cannot be used for new things - ('unusable', 'Unusable'), # Has some kind of error + ('creating', 'Creating'), # Creating VM/image/etc. + ('active', 'Active'), # Is usable / active + ('disabled', 'Disabled'), # Is usable, but cannot be used for new things + ('unusable', 'Unusable'), # Has some kind of error ('deleted', 'Deleted'), # Does not exist anymore, only DB entry as a log ) -STATUS_DEFAULT='pending' +STATUS_DEFAULT = 'pending' + class VMHost(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -30,19 +35,13 @@ class VMHost(models.Model): # ram that can be used of the server usable_ram_in_gb = models.FloatField(default=0) - - status = models.CharField(max_length=32, - choices=STATUS_CHOICES, - default=STATUS_DEFAULT - ) + status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_DEFAULT) class VMProduct(Product): - vmhost = models.ForeignKey(VMHost, - on_delete=models.CASCADE, - editable=False, - blank=True, - null=True) + vmhost = models.ForeignKey( + VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True + ) cores = models.IntegerField() ram_in_gb = models.FloatField() @@ -60,36 +59,30 @@ class VMDiskImageProduct(models.Model): """ - uuid = models.UUIDField(primary_key=True, - default=uuid.uuid4, - editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) + 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) - size_in_gb = models.FloatField(null=True, - blank=True) - import_url = models.URLField(null=True, - blank=True) + size_in_gb = models.FloatField(null=True, blank=True) + import_url = models.URLField(null=True, blank=True) - - storage_class = models.CharField(max_length=32, - choices = ( - ('hdd', 'HDD'), - ('ssd', 'SSD'), - ), - default='ssd' + storage_class = models.CharField( + max_length=32, + choices=( + ('hdd', 'HDD'), + ('ssd', 'SSD'), + ), + default='ssd' ) - status = models.CharField(max_length=32, - choices=STATUS_CHOICES, - default=STATUS_DEFAULT + status = models.CharField( + max_length=32, choices=STATUS_CHOICES, default=STATUS_DEFAULT ) + class VMDiskProduct(models.Model): """ The VMDiskProduct is attached to a VM. @@ -104,14 +97,29 @@ class VMDiskProduct(models.Model): on_delete=models.CASCADE, editable=False) - vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) + vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) image = models.ForeignKey(VMDiskImageProduct, on_delete=models.CASCADE) size_in_gb = models.FloatField(blank=True) + # Sample code for clean method + + # Ensures that a VMDiskProduct can only be created from a VMDiskImageProduct + # that is in status 'active' + + # 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) + vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) mac_address = models.IntegerField() diff --git a/uncloud/uncloud_vm/tests.py b/uncloud/uncloud_vm/tests.py new file mode 100644 index 0000000..a9ca5ee --- /dev/null +++ b/uncloud/uncloud_vm/tests.py @@ -0,0 +1,107 @@ +import datetime + +from django.test import TestCase +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.core.exceptions import ValidationError + +from uncloud_vm.models import VMDiskImageProduct, VMDiskProduct, VMProduct, VMHost +from uncloud_pay.models import Order + +User = get_user_model() + + +# If you want to check the test database then use the following connecting parameters + +# host: localhost +# database: test_uncloud +# user: root +# password: +# port: 5432 + +class VMTestCase(TestCase): + @classmethod + def setUpClass(cls): + # Setup vm host + cls.vm_host, created = VMHost.objects.get_or_create( + hostname='server1.place11.ungleich.ch', physical_cores=32, usable_cores=320, + usable_ram_in_gb=512.0, status='active' + ) + super().setUpClass() + + def setUp(self) -> None: + # Setup two users as it is common to test with different user + self.user = User.objects.create_user( + username='testuser', email='test@test.com', first_name='Test', last_name='User' + ) + self.user2 = User.objects.create_user( + username='Meow', email='meow123@test.com', first_name='Meow', last_name='Cat' + ) + super().setUp() + + def create_sample_vm(self, owner): + return VMProduct.objects.create( + vmhost=self.vm_host, cores=2, ram_in_gb=4, owner=owner, + order=Order.objects.create( + owner=owner, + creation_date=datetime.datetime.now(tz=timezone.utc), + starting_date=datetime.datetime.now(tz=timezone.utc), + ending_date=datetime.datetime(2020, 4, 2, tzinfo=timezone.utc), + recurring_price=4.0, one_time_price=5.0, recurring_period='per_month' + ) + ) + + def test_disk_product(self): + """Ensures that a VMDiskProduct can only be created from a VMDiskImageProduct + that is in status 'active'""" + + vm = self.create_sample_vm(owner=self.user) + + pending_disk_image = VMDiskImageProduct.objects.create( + owner=self.user, name='alpine3.11', is_os_image=True, is_public=True, size_in_gb=10, + status='pending' + ) + try: + vm_disk_product = VMDiskProduct.objects.create( + owner=self.user, vm=vm, image=pending_disk_image, size_in_gb=10 + ) + except ValidationError: + vm_disk_product = None + + self.assertIsNone( + vm_disk_product, + msg='VMDiskProduct created with disk image whose status is not active.' + ) + + def test_vm_disk_product_creation(self): + """Ensure that a user can only create a VMDiskProduct for an existing VM""" + + disk_image = VMDiskImageProduct.objects.create( + owner=self.user, name='alpine3.11', is_os_image=True, is_public=True, size_in_gb=10, + status='active' + ) + + with self.assertRaises(ValidationError, msg='User created a VMDiskProduct for non-existing VM'): + # Create VMProduct object but don't save it in database + vm = VMProduct() + + vm_disk_product = VMDiskProduct.objects.create( + owner=self.user, vm=vm, image=disk_image, size_in_gb=10 + ) + + def test_vm_disk_product_creation_for_someone_else(self): + """Ensure that a user can only create a VMDiskProduct for his/her own VM""" + + # Create a VM which is ownership of self.user2 + someone_else_vm = self.create_sample_vm(owner=self.user2) + + # 'self.user' would try to create a VMDiskProduct for 'user2's VM + with self.assertRaises(ValidationError, msg='User created a VMDiskProduct for someone else VM.'): + vm_disk_product = VMDiskProduct.objects.create( + owner=self.user, vm=someone_else_vm, + size_in_gb=10, + image=VMDiskImageProduct.objects.create( + owner=self.user, name='alpine3.11', is_os_image=True, is_public=True, size_in_gb=10, + status='active' + ) + )