diff --git a/opennebula_api/models.py b/opennebula_api/models.py index c93e4f9d..6a7ba2fc 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -14,11 +14,97 @@ class VirtualMachineTemplate(models.Model): core_price = models.FloatField() disk_size_price = models.FloatField() + def calculate_price(self): + template = OpenNebulaManager()._get_template(self.opennebula_id).template + + price = int(template.vcpu) * self.core_price + price += int(template.memory) / 1024 * self.memory_price + try: + price += int(template.disk.size) / 1024 * self.disk_size_price + except AttributeError: + for disk in template.disks: + price += int(disk.size) / 1024 * self.disk_size_price + return price + + def get_name(self): + + template = OpenNebulaManager()._get_template(template_id=self.opennebula_id) + return template.name + + def get_cores(self): + + template = OpenNebulaManager()._get_template(template_id=self.opennebula_id).template + return int(template.vcpu) + + def get_disk_size(self): + + template = OpenNebulaManager()._get_template(template_id=self.opennebula_id).template + disk_size = 0 + for disk in template.disks: + disk_size += int(disk.size) + return disk_size / 1024 + + def get_memory(self): + + template = OpenNebulaManager()._get_template(template_id=self.opennebula_id).template + return int(template.memory) / 1024 class VirtualMachine(models.Model): """This class represents an opennebula virtual machine.""" opennebula_id = models.IntegerField() - template = models.ForeignKey(VirtualMachineTemplate) + vm_template = models.ForeignKey(VirtualMachineTemplate) + + VM_STATE = { + '0': 'INIT', + '1': 'PENDING', + '2': 'HOLD', + '3': 'ACTIVE', + '4': 'STOPPED', + '5': 'SUSPENDED', + '6': 'DONE', + '8': 'POWEROFF', + '9': 'UNDEPLOYED', + '10': 'CLONING', + '11': 'CLONING_FAILURE', + } + + def get_name(self): + + vm = OpenNebulaManager()._get_vm(vm_id=self.opennebula_id) + return vm.name + + def get_cores(self): + + return self.vm_template.get_cores() + + def get_disk_size(self): + + return self.vm_template.get_disk_size() + + def get_memory(self): + + return self.vm_template.get_memory() + + def get_id(self): + + vm = OpenNebulaManager()._get_vm(vm_id=self.opennebula_id) + return vm.id + + def get_ip(self): + + vm = OpenNebulaManager()._get_vm(vm_id=self.opennebula_id) + try: + return vm.user_template.ungleich_public_ip + except AttributeError: + return '-' + + def get_state(self): + + vm = OpenNebulaManager()._get_vm(vm_id=self.opennebula_id) + return self.VM_STATE.get(str(vm.state)) + + def get_price(self): + return self.vm_template.calculate_price() class OpenNebulaManager(): """This class represents an opennebula manager.""" @@ -31,8 +117,7 @@ class OpenNebulaManager(): settings.OPENNEBULA_PASSWORD ) - - if not create_user: + if not create_user or email is None: return # Get or create oppenebula user using given credentials @@ -67,35 +152,122 @@ class OpenNebulaManager(): except WrongNameError as wrong_name_err: opennebula_user = self.oneadmin_client.call(oca.User.METHODS['allocate'], email, password, 'core') + return opennebula_user + #TODO: Replace with logger except ConnectionRefusedError: print('Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) - return opennebula_user + raise ConnectionRefusedError def _get_user_pool(self): try: user_pool = oca.UserPool(self.oneadmin_client) user_pool.info() + #TODO: Replace with logger except ConnectionRefusedError: print('Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) + raise ConnectionRefusedError return user_pool - def create_virtualmachine(self, template_id): - template_pool = oca.VmTemplatePool(self.oneadmin_client) - template_pool.info() + def _get_vm_pool(self): + try: + vm_pool = oca.VirtualMachinePool(self.oneadmin_client) + vm_pool.info() + #TODO: Replace with logger + except ConnectionRefusedError: + print('Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) + ) + raise ConnectionRefusedError + return vm_pool + + + def _get_vm(self, vm_id): + vm_pool = self._get_vm_pool() + # Get virtual machines from all users + vm_pool.info(filter=-2) + return vm_pool.get_by_id(vm_id) + + def create_vm(self, template_id): + template_pool = self._get_template_pool() template = template_pool.get_by_id(template_id) vm_id = template.instantiate() - self.oneadmin.call( - oca.VirtualMachine.METHODS['chown'], - vm_id, - self.opennebula_user.id, - self.opennebula_user.group_ids[0] - ) + try: + self.oneadmin_client.call( + oca.VirtualMachine.METHODS['chown'], + vm_id, + self.opennebula_user.id, + self.opennebula_user.group_ids[0] + ) + except AttributeError: + pass return vm_id + def delete_vm(self, vm_id): + vm = self._get_vm(vm_id) + vm.delete() + + def _get_template_pool(self): + try: + template_pool = oca.VmTemplatePool(self.oneadmin_client) + template_pool.info() + #TODO: Replace with logger + except ConnectionRefusedError: + print('Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) + ) + raise ConnectionRefusedError + return template_pool + + def _get_template(self, template_id): + template_pool = self._get_template_pool() + return template_pool.get_by_id(template_id) + + + + def create_template(self, name, cores, memory, disk_size): + """Create and add a new template to opennebula. + :param name: A string representation describing the template. + Used as label in view. + :param cores: Amount of virtual cpu cores for the VM. + :param memory: Amount of RAM for the VM (MB) + :param disk_size: Amount of disk space for VM (MB) + """ + template_string_formatter = """ + """ + template_id = oca.VmTemplate.allocate( + self.oneadmin_client, + template_string_formatter.format( + name=name, + vcpu=cores, + cpu=0.1*cores, + size=1024 * disk_size, + memory=1024 * memory + ) + ) + + return template_id + + def delete_template(self, template_id): + self.oneadmin_client.call(oca.VmTemplate.METHODS['delete'], template_id, False) + + + diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py new file mode 100644 index 00000000..e8ff33bd --- /dev/null +++ b/opennebula_api/serializers.py @@ -0,0 +1,93 @@ +import oca + +from rest_framework import serializers + +from oca import OpenNebulaException + +from .models import VirtualMachine, VirtualMachineTemplate, OpenNebulaManager + +class VirtualMachineTemplateSerializer(serializers.ModelSerializer): + """Serializer to map the virtual machine template instance into JSON format.""" + cores = serializers.IntegerField(source='get_cores') + name = serializers.CharField(source='get_name') + disk_size = serializers.IntegerField(source='get_disk_size') + memory = serializers.IntegerField(source='get_memory') + + class Meta: + model = VirtualMachineTemplate + fields = ('id', 'name', 'cores', 'memory', 'disk_size', 'base_price', + 'core_price', 'memory_price', 'disk_size_price', 'opennebula_id') + read_only_fields = ('opennebula_id', ) + + def validate(self, data): + # Create the opennebula model + cores = data.pop('get_cores') + name = data.pop('get_name') + disk_size = data.pop('get_disk_size') + memory = data.pop('get_memory') + + manager = OpenNebulaManager(create_user = False) + + try: + opennebula_id = manager.create_template(name=name, cores=cores, + memory=memory, + disk_size=disk_size) + data.update({'opennebula_id':opennebula_id}) + except OpenNebulaException as err: + raise serializers.ValidationError("OpenNebulaException occured. {0}".format(err)) + + return data + + def create(self, validated_data): + return VirtualMachineTemplate.objects.create(**validated_data) + +class TemplatePrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): + def display_value(self, instance): + return 'Template: {}'.format(instance.get_name()) + +class VirtualMachineSerializer(serializers.ModelSerializer): + """Serializer to map the virtual machine instance into JSON format.""" + + #TODO: Maybe we can change to template.get_cores + cores = serializers.IntegerField(read_only=True, source='get_cores') + name = serializers.CharField(read_only=True, source='get_name') + disk_size = serializers.IntegerField(read_only=True, source='get_disk_size') + memory = serializers.IntegerField(read_only=True, source='get_memory') + #TODO: See if we can change to IPAddressField + ip = serializers.CharField(read_only=True, source='get_ip') + deploy_id = serializers.IntegerField(read_only=True, source='get_deploy_id') + vm_id = serializers.IntegerField(read_only=True, source='get_vm_id') + state = serializers.CharField(read_only=True, source='get_state') + price = serializers.FloatField(read_only=True, source='get_price') + + vm_template = VirtualMachineTemplateSerializer(read_only=True) + + vm_template_id = TemplatePrimaryKeyRelatedField( + queryset=VirtualMachineTemplate.objects.all(), + source='vm_template' + ) + + class Meta: + model = VirtualMachine + fields = ('id', 'opennebula_id', 'vm_template', 'vm_template_id', 'cores', 'name', + 'disk_size', 'memory', 'ip', 'deploy_id', 'state', 'vm_id', + 'price') + read_only_fields = ('opennebula_id', ) + + def validate(self, data): + # Create the opennebula model + manager = OpenNebulaManager(create_user = False) + + try: + template_id = data['vm_template'].opennebula_id + opennebula_id = manager.create_vm(template_id) + data.update({'opennebula_id':opennebula_id}) + except OpenNebulaException as err: + raise serializers.ValidationError("OpenNebulaException occured. {0}".format(err)) + + return data + + def create(self, validated_data): + return VirtualMachine.objects.create(**validated_data) + + diff --git a/opennebula_api/tests.py b/opennebula_api/tests.py index 6852fd1b..14694e6f 100644 --- a/opennebula_api/tests.py +++ b/opennebula_api/tests.py @@ -18,7 +18,11 @@ class OpenNebulaManagerTestCases(TestCase): def test_model_can_connect_to_server(self): """Test the opennebula manager model can connect to a server.""" - self.assertFalse(self.manager is None) + try: + user_pool = self.manager._get_user_pool() + except: + user_pool = None + self.assertFalse(user_pool is None) def test_model_can_create_user(self): """Test the opennebula manager model can create a new user.""" @@ -26,7 +30,12 @@ class OpenNebulaManagerTestCases(TestCase): self.manager = OpenNebulaManager(email=self.email, password=self.password, create_user=True) - new_count = len(self.manager._get_user_pool()) + user_pool = self.manager._get_user_pool() + new_count = len(user_pool) + # Remove the user afterwards + user = user_pool.get_by_name(self.email) + user.delete() + self.assertNotEqual(old_count, new_count) @@ -46,8 +55,10 @@ class VirtualMachineTemplateTestCase(TestCase): self.disk_size = 10.0 self.manager = OpenNebulaManager(email=None, password=None, create_user=False) - self.opennebula_id = self.manager.create_template(self.cores, self.memory, - self.disk_size) + self.opennebula_id = self.manager.create_template(name=self.template_name, + cores=self.cores, + memory=self.memory, + disk_size=self.disk_size) self.template = VirtualMachineTemplate(opennebula_id=self.opennebula_id, base_price=self.base_price, @@ -61,14 +72,42 @@ class VirtualMachineTemplateTestCase(TestCase): old_count = VirtualMachineTemplate.objects.count() self.template.save() new_count = VirtualMachineTemplate.objects.count() + # Remove the template afterwards + template = self.manager._get_template(self.template.opennebula_id) + template.delete() self.assertNotEqual(old_count, new_count) + def test_model_can_calculate_price(self): + price = self.cores * self.core_price + price += self.memory * self.memory_price + price += self.disk_size * self.disk_size_price + self.assertEqual(price, self.template.calculate_price()) + + class VirtualMachineTestCase(TestCase): def setUp(self): """Define the test client and other test variables.""" - self.manager = OpenNebulaManager(email=None, password=None, create_user=False) - self.template = VirtualMachineTemplate.objects.first() + self.template_name = "Standard" + self.base_price = 0.0 + self.core_price = 5.0 + self.memory_price = 2.0 + self.disk_size_price = 0.6 + + self.cores = 1 + self.memory = 1 + self.disk_size = 10.0 + self.manager = OpenNebulaManager(email=None, password=None, create_user=False) + self.opennebula_id = self.manager.create_template(name=self.template_name, + cores=self.cores, + memory=self.memory, + disk_size=self.disk_size) + + self.template = VirtualMachineTemplate(opennebula_id=self.opennebula_id, + base_price=self.base_price, + memory_price=self.memory_price, + core_price=self.core_price, + disk_size_price=self.disk_size_price) self.template_id = self.template.opennebula_id() self.opennebula_id = self.manager.create_virtualmachine(template_id=self.template_id) @@ -82,6 +121,9 @@ class VirtualMachineTestCase(TestCase): new_count = VirtualMachine.objects.count() self.assertNotEqual(old_count, new_count) + def test_model_can_create_a_virtualmachine_for_user(self): + pass + def test_model_can_delete_a_virtualmachine(self): """Test the virtualmachine model can delete a virtualmachine.""" self.virtualmachine.save() diff --git a/opennebula_api/views.py b/opennebula_api/views.py index 91ea44a2..4cd237ae 100644 --- a/opennebula_api/views.py +++ b/opennebula_api/views.py @@ -1,3 +1,35 @@ -from django.shortcuts import render +from rest_framework import generics -# Create your views here. +from .serializers import VirtualMachineTemplateSerializer, \ + VirtualMachineSerializer +from .models import VirtualMachineTemplate, VirtualMachine, OpenNebulaManager + +class TemplateCreateView(generics.ListCreateAPIView): + """This class defines the create behavior of our rest api.""" + queryset = VirtualMachineTemplate.objects.all() + serializer_class = VirtualMachineTemplateSerializer + + def perform_create(self, serializer): + """Save the post data when creating a new template.""" + serializer.save() + +class TemplateDetailsView(generics.RetrieveUpdateDestroyAPIView): + """This class handles the http GET, PUT and DELETE requests.""" + + queryset = VirtualMachineTemplate.objects.all() + serializer_class = VirtualMachineTemplateSerializer + +class VmCreateView(generics.ListCreateAPIView): + """This class defines the create behavior of our rest api.""" + queryset = VirtualMachine.objects.all() + serializer_class = VirtualMachineSerializer + + def perform_create(self, serializer): + """Save the post data when creating a new template.""" + serializer.save() + +class VmDetailsView(generics.RetrieveUpdateDestroyAPIView): + """This class handles the http GET, PUT and DELETE requests.""" + + queryset = VirtualMachine.objects.all() + serializer_class = VirtualMachineSerializer diff --git a/requirements.txt b/requirements.txt index cec95282..de1b1500 100644 --- a/requirements.txt +++ b/requirements.txt @@ -82,7 +82,7 @@ stripe==1.33.0 wheel==0.29.0 django-admin-honeypot==1.0.0 coverage==4.3.4 -git+https://github.com/python-oca/python-oca.git#egg=python-oca +git+https://github.com/ungleich/python-oca.git#egg=python-oca djangorestframework