From f142bb1ce980e68419ac67d57e2b9af2d2079529 Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Mon, 29 May 2017 15:00:47 +0200
Subject: [PATCH 01/10] Move keys section underneath user name menu

---
 hosting/templates/hosting/base_short.html | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/hosting/templates/hosting/base_short.html b/hosting/templates/hosting/base_short.html
index c6d1772e..4a1a95be 100644
--- a/hosting/templates/hosting/base_short.html
+++ b/hosting/templates/hosting/base_short.html
@@ -72,11 +72,7 @@
                                 <i class="fa fa-credit-card"></i> {% trans "My Orders"%}
                             </a>
                         </li>
-                        <li>
-                            <a href="{% url 'hosting:key_pair' %}">
-                                <i class="fa fa-key" aria-hidden="true"></i> {% trans "Keys"%}
-                            </a>
-                        </li>
+                        
                         <li>
                             <a href="{% url 'hosting:notifications' %}">
                                 <i class="fa fa-bell"></i> {% trans "Notifications "%}
@@ -87,6 +83,11 @@
                             <i class="glyphicon glyphicon-user"></i> {{request.user.name}} <span class="caret"></span></a>
                           <ul id="g-account-menu" class="dropdown-menu" role="menu">
                             <li><a href="{% url 'hosting:logout' %}"><i class="glyphicon glyphicon-lock"></i>{% trans "Logout"%} </a></li>
+							<li>
+                            <a href="{% url 'hosting:key_pair' %}">
+                                <i class="fa fa-key"></i> {% trans "Keys"%}
+                            </a>
+                        	</li>
                           </ul>
                         </li>
                     {% else %}

From 0b5cf2c057c4636bc9e7789a25739dcf2e2f8cc8 Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Wed, 31 May 2017 18:01:54 +0200
Subject: [PATCH 02/10] Cleanup and add tests

---
 opennebula_api/models.py | 133 ++++++++++++++++++++++++++++++-
 opennebula_api/tests.py  | 166 +++++++++++++++------------------------
 2 files changed, 192 insertions(+), 107 deletions(-)

diff --git a/opennebula_api/models.py b/opennebula_api/models.py
index 3eac0b6e..423013e4 100644
--- a/opennebula_api/models.py
+++ b/opennebula_api/models.py
@@ -2,12 +2,14 @@ import oca
 import socket
 import logging
 
+from oca.pool import WrongNameError
+from oca.exceptions import OpenNebulaException
 
 from django.conf import settings
 from django.utils.functional import cached_property
 
-from oca.pool import WrongNameError
-from oca.exceptions import OpenNebulaException
+from utils.models import CustomUser
+
 logger = logging.getLogger(__name__)
 
 
@@ -35,10 +37,33 @@ class OpenNebulaManager():
             )
         except:
             pass
+    def _get_client(self, user):
+        """Get a opennebula client object for a CustomUser object 
+        
+        Args:
+            user (CustomUser): dynamicweb CustomUser object
+
+        Returns:
+            oca.Client: Opennebula client object
+
+        Raise:
+            ConnectionError: If the connection to the opennebula server can't be
+                established 
+        """
+        return oca.Client("{0}:{1}".format(
+            user.email,
+            user.password),
+            "{protocol}://{domain}:{port}{endpoint}".format(
+                protocol=settings.OPENNEBULA_PROTOCOL,
+                domain=settings.OPENNEBULA_DOMAIN,
+                port=settings.OPENNEBULA_PORT,
+                endpoint=settings.OPENNEBULA_ENDPOINT
+        ))
 
     def _get_opennebula_client(self, username, password):
         return oca.Client("{0}:{1}".format(
             username,
+
             password),
             "{protocol}://{domain}:{port}{endpoint}".format(
                 protocol=settings.OPENNEBULA_PROTOCOL,
@@ -47,6 +72,69 @@ class OpenNebulaManager():
                 endpoint=settings.OPENNEBULA_ENDPOINT
         ))
 
+    def _get_user(self, user):
+        """Get the corresponding opennebula user for a CustomUser object 
+        
+        Args:
+            user (CustomUser): dynamicweb CustomUser object
+
+        Returns:
+            oca.User: Opennebula user object
+
+        Raise:
+            WrongNameError: If no openebula user with this credentials exists
+            ConnectionError: If the connection to the opennebula server can't be
+                established 
+        """
+        user_pool = self._get_user_pool()
+        return user_pool.get_by_name(user.email)
+
+    def create_user(self, user: CustomUser):
+        """Create a new opennebula user or a corresponding CustomUser object
+
+        
+        Args:
+            user (CustomUser): dynamicweb CustomUser object
+
+        Returns:
+            int: Return the opennebula user id
+            
+        Raises:
+            ConnectionError: If the connection to the opennebula server can't be
+                established 
+            UserExistsError: If a user with this credeintals already exits on the
+                server
+            UserCredentialError: If a user with this email exists but the
+                password is worng
+
+        """
+        try:
+            self._get_user(user)
+            try: 
+                self._get_client(self, user)
+                logger.debug('User already exists')
+                raise UserExistsError()
+            except OpenNebulaException as err:
+                logger.error('OpenNebulaException error: {0}'.format(err))
+                logger.debug('User exists but password is wrong')
+                raise UserCredentialError()
+
+        except WrongNameError:
+            user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'],
+                user.email, user.password, 'core')
+            logger.debug('Created a user for CustomObject: {user} with user id = {u_id}',
+                user=user,
+                u_id=user_id
+            )
+            return user_id 
+        except ConnectionRefusedError:
+            logger.error('Could not connect to host: {host} via protocol {protocol}'.format(
+                host=settings.OPENNEBULA_DOMAIN,
+                protocol=settings.OPENNEBULA_PROTOCOL)
+            )
+            raise ConnectionRefusedError
+        
+
     def _get_or_create_user(self, email, password):
         try:
             user_pool = self._get_user_pool()
@@ -77,7 +165,7 @@ class OpenNebulaManager():
                 host=settings.OPENNEBULA_DOMAIN,
                 protocol=settings.OPENNEBULA_PROTOCOL)
             )
-            raise ConnectionRefusedError
+            raise 
         return user_pool
 
     def _get_vm_pool(self):
@@ -350,3 +438,42 @@ class OpenNebulaManager():
             self.opennebula_user.id,
             new_password
         )
+
+    def add_public_key(self, user, public_key='', replace=True):
+        """ 
+
+        Args: 
+            user (CustomUser): Dynamicweb user 
+            public_key (string): Public key to add to the user
+            replace (bool): Optional if True the new public key replaces the old
+
+        Raises:
+            KeyExistsError: If replace is False and the user already has a
+                public key 
+            WrongNameError: If no openebula user with this credentials exists
+            ConnectionError: If the connection to the opennebula server can't be
+                established 
+
+        Returns:
+            True if public_key was added
+
+        """
+        # TODO: Check if we can remove this first try because we basically just
+        # raise the possible Errors 
+        try:
+            open_user = self._get_user(user)
+            try:
+                old_key = open_user.template.ssh_public_key 
+                if not replace:
+                    raise KeyExistsError()
+
+            except AttributeError:
+                pass
+            self.oneadmin_client.call('user.update', open_user.id,
+                         '<CONTEXT><SSH_PUBLIC_KEY>{key}</SSH_PUBLIC_KEY></CONTEXT>'.format(key=public_key))
+            return True
+        except WrongNameError:
+            raise
+
+        except ConnectionError:
+            raise
diff --git a/opennebula_api/tests.py b/opennebula_api/tests.py
index 14694e6f..bd744a66 100644
--- a/opennebula_api/tests.py
+++ b/opennebula_api/tests.py
@@ -1,35 +1,53 @@
+import socket
+import random
+import string
+
 from django.test import TestCase
-from .models import VirtualMachine, VirtualMachineTemplate, OpenNebulaManager
+
+from .models import OpenNebulaManager
+from utils.models import CustomUser
 
 class OpenNebulaManagerTestCases(TestCase):
     """This class defines the test suite for the opennebula manager model."""
 
     def setUp(self):
         """Define the test client and other test variables."""
-        self.cores = 1 
-        self.memory = 1
-        self.disk_size = 10.0
         
-        self.email = 'test@test.com'
-        self.password = 'testtest'
 
-        self.manager = OpenNebulaManager(email=None, password=None, create_user=False) 
+        self.email = '{}@ungleich.ch'.format(''.join(random.choices(string.ascii_uppercase, k=10)))
+        self.password = ''.join(random.choices(string.ascii_uppercase + string.digits, k=20)) 
+
+        self.user = CustomUser.objects.create(name='test', email=self.email,
+                password=self.password) 
+
+        self.vm_specs = {}
+        self.vm_specs['cpu'] = 1
+        self.vm_specs['memory'] = 2
+        self.vm_specs['disk_size'] = 10
+
+        self.manager = OpenNebulaManager() 
 
 
-    def test_model_can_connect_to_server(self):
-        """Test the opennebula manager model can connect to a server."""
+    def test_connect_to_server(self):
+        """Test the opennebula manager can connect to a server."""
         try:
-            user_pool = self.manager._get_user_pool()
-        except:
-            user_pool = None
-        self.assertFalse(user_pool is None)
+            ver = self.manager.oneadmin_client.version()
+        except: 
+            ver = None
+        self.assertTrue(ver is not None)
 
-    def test_model_can_create_user(self):
-        """Test the opennebula manager model can create a new user."""
+    def test_get_user(self):
+        """Test the opennebula manager can get a existing user."""
+        self.manager.create_user(self.user)
+        user = self.manager._get_user(self.user)
+        name = user.name
+        self.assertNotEqual(name, None)
+
+    def test_create_and_delete_user(self):
+        """Test the opennebula manager can create and delete a new user."""
         old_count = len(self.manager._get_user_pool())
         self.manager = OpenNebulaManager(email=self.email,
-                                         password=self.password,
-                                         create_user=True)
+                                         password=self.password)
         user_pool = self.manager._get_user_pool()
         new_count = len(user_pool)
         # Remove the user afterwards
@@ -38,96 +56,36 @@ class OpenNebulaManagerTestCases(TestCase):
         
         self.assertNotEqual(old_count, new_count)
 
+    def test_user_can_login(self):
+        """ Test the manager can login to a new created user"""
+        self.manager.create_user(self.user)
+        user = self.manager._get_user(self.user)
+        client = self.manager._get_client(self.user)
+        version = client.version()
 
-class VirtualMachineTemplateTestCase(TestCase):
-    """This class defines the test suite for the virtualmachine template model."""
+        # Cleanup 
+        user.delete()
+        self.assertNotEqual(version, None)
 
-    def setUp(self):
-        """Define the test client and other test variables."""
-        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
+    def test_add_public_key_to_user(self):
+        """ Test the manager can add a new public key to an user """
+        self.manager.create_user(self.user)
+        user = self.manager._get_user(self.user)
+        public_key = 'test'
+        self.manager.add_public_key(self.user, public_key)
+        # Fetch new user information from opennebula
+        user.info()
+        user_public_key = user.template.ssh_public_key
+        # Cleanup 
+        user.delete()
 
-        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)
-
-
-    def test_model_can_create_a_virtualmachine_template(self):
-        """Test the virtualmachine template model can create a template."""
-        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.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)
-                                           
-        self.virtualmachine = VirtualMachine(opennebula_id=self.opennebula_id,
-                                             template=self.template)
+        self.assertEqual(user_public_key, public_key)
         
-    def test_model_can_create_a_virtualmachine(self):
-        """Test the virtualmachine model can create a virtualmachine."""
-        old_count = VirtualMachine.objects.count()
-        self.virtualmachine.save()
-        new_count = VirtualMachine.objects.count()
-        self.assertNotEqual(old_count, new_count)
 
-    def test_model_can_create_a_virtualmachine_for_user(self):
-        pass
+    def test_requires_ssh_key_for_new_vm(self):
+        """Test the opennebula manager requires the user to have a ssh key when
+        creating a new vm"""
+
+
+
 
-    def test_model_can_delete_a_virtualmachine(self):
-        """Test the virtualmachine model can delete a virtualmachine."""
-        self.virtualmachine.save()
-        old_count = VirtualMachine.objects.count()
-        VirtualMachine.objects.first().delete()
-        new_count = VirtualMachine.objects.count()
-        self.assertNotEqual(old_count, new_count)

From ca63355914892e115b97f958afb4251ae69745fd Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Thu, 1 Jun 2017 17:30:53 +0200
Subject: [PATCH 03/10] Remove templates

In the current setup the templates views are not necessary
---
 opennebula_api/serializers.py | 37 -----------------------------------
 opennebula_api/urls.py        |  7 +------
 opennebula_api/views.py       | 32 ------------------------------
 3 files changed, 1 insertion(+), 75 deletions(-)

diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py
index 6c86776a..60ce16f9 100644
--- a/opennebula_api/serializers.py
+++ b/opennebula_api/serializers.py
@@ -11,37 +11,12 @@ from .models import OpenNebulaManager
 class VirtualMachineTemplateSerializer(serializers.Serializer):
     """Serializer to map the virtual machine template instance into JSON format."""
     id          = serializers.IntegerField(read_only=True)
-    set_name    = serializers.CharField(read_only=True, label='Name')
     name        = serializers.SerializerMethodField()
     cores       = serializers.SerializerMethodField() 
-    disk        = serializers.IntegerField(write_only=True)
     disk_size   = serializers.SerializerMethodField()
-    set_memory      = serializers.IntegerField(write_only=True, label='Memory')
     memory      = serializers.SerializerMethodField()
     price       = serializers.SerializerMethodField()
 
-    def create(self, validated_data):
-        data = validated_data
-        template = data.pop('template')
-
-        cores = template.pop('vcpu')
-        name    = data.pop('name')
-        disk_size = data.pop('disk') 
-        memory  = template.pop('memory')
-        manager = OpenNebulaManager()
-        
-        try:
-            opennebula_id = manager.create_template(name=name, cores=cores,
-                                                    memory=memory,
-                                                    disk_size=disk_size,
-                                                    core_price=core_price,
-                                                    disk_size_price=disk_size_price,
-                                                    memory_price=memory_price)
-        except OpenNebulaException as err:
-            raise serializers.ValidationError("OpenNebulaException occured. {0}".format(err))
-        
-        return manager.get_template(template_id=opennebula_id)
-
     def get_cores(self, obj):
         if hasattr(obj.template, 'vcpu'):
             return obj.template.vcpu
@@ -58,18 +33,6 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
         except:
             return 0
 
-
-    def get_price(self, obj):
-        template = obj.template
-        price = float(template.cpu) * 5.0
-        price += (int(template.memory)/1024 * 2.0)
-        try:
-            for disk in template.disks:
-                price += int(disk.size)/1024 * 0.6
-        except:
-            pass
-        return price
-
     def get_memory(self, obj):
         return int(obj.template.memory)/1024
 
diff --git a/opennebula_api/urls.py b/opennebula_api/urls.py
index be5bdbf2..87976e2e 100644
--- a/opennebula_api/urls.py
+++ b/opennebula_api/urls.py
@@ -1,15 +1,10 @@
 from django.conf.urls import url, include
 from rest_framework.urlpatterns import format_suffix_patterns
-from .views import TemplateCreateView, TemplateDetailsView,\
-                   VmCreateView, VmDetailsView
+from .views import VmCreateView, VmDetailsView
 
 urlpatterns = {
     url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
 
-    url(r'^templates/$', TemplateCreateView.as_view(), name="template_create"),
-    url(r'^templates/(?P<pk>[0-9]+)/$', TemplateDetailsView.as_view(),
-        name="templates_details"),
-    
     url(r'^vms/$', VmCreateView.as_view(), name="vm_create"),
     url(r'^vms/(?P<pk>[0-9]+)/$', VmDetailsView.as_view(),
         name="vm_details"),
diff --git a/opennebula_api/views.py b/opennebula_api/views.py
index d982f7bb..f6c6b8d7 100644
--- a/opennebula_api/views.py
+++ b/opennebula_api/views.py
@@ -20,38 +20,6 @@ class ServiceUnavailable(APIException):
     default_code = 'service_unavailable'
 
 
-class TemplateCreateView(generics.ListCreateAPIView):
-    """This class handles the GET and POST requests."""
-
-    serializer_class = VirtualMachineTemplateSerializer
-    permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser)
-
-    def get_queryset(self):
-        manager = OpenNebulaManager()
-        return manager.get_templates()
-
-
-    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."""
-
-    serializer_class = VirtualMachineTemplateSerializer
-    permission_classes = (permissions.IsAuthenticated)
-
-    def get_queryset(self):
-        manager = OpenNebulaManager()
-        # We may have ConnectionRefusedError if we don't have a 
-        # connection to OpenNebula. For now, we raise ServiceUnavailable
-        try:
-            templates = manager.get_templates()
-        except ConnectionRefusedError:
-            raise ServiceUnavailable            
-        
-        return templates
-
 class VmCreateView(generics.ListCreateAPIView):
     """This class handles the GET and POST requests."""
     serializer_class = VirtualMachineSerializer

From 0db15d99a5382dcfe6d641c867d15872d0dbf61c Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Thu, 1 Jun 2017 19:08:38 +0200
Subject: [PATCH 04/10] Add exceptions, merge for ssh key

The replace variable of the add_public_key function is now a merge
variable in order to append new keys to a user
---
 opennebula_api/exceptions.py |  9 +++++++++
 opennebula_api/models.py     |  6 ++++--
 opennebula_api/tests.py      | 17 +++++++++++++++++
 3 files changed, 30 insertions(+), 2 deletions(-)
 create mode 100644 opennebula_api/exceptions.py

diff --git a/opennebula_api/exceptions.py b/opennebula_api/exceptions.py
new file mode 100644
index 00000000..2fa15b43
--- /dev/null
+++ b/opennebula_api/exceptions.py
@@ -0,0 +1,9 @@
+
+class KeyExistsError(Exception):
+    pass
+
+class UserExistsError(Exception):
+    pass
+
+class UserCredentialError(Exception):
+    pass
diff --git a/opennebula_api/models.py b/opennebula_api/models.py
index 423013e4..052291c1 100644
--- a/opennebula_api/models.py
+++ b/opennebula_api/models.py
@@ -9,6 +9,7 @@ from django.conf import settings
 from django.utils.functional import cached_property
 
 from utils.models import CustomUser
+from .exceptions import KeyExistsError, UserExistsError, UserCredentialError
 
 logger = logging.getLogger(__name__)
 
@@ -439,7 +440,7 @@ class OpenNebulaManager():
             new_password
         )
 
-    def add_public_key(self, user, public_key='', replace=True):
+    def add_public_key(self, user, public_key='', merge=False):
         """ 
 
         Args: 
@@ -464,8 +465,9 @@ class OpenNebulaManager():
             open_user = self._get_user(user)
             try:
                 old_key = open_user.template.ssh_public_key 
-                if not replace:
+                if not merge:
                     raise KeyExistsError()
+                public_key += '\n{key}'.format(key=old_key)
 
             except AttributeError:
                 pass
diff --git a/opennebula_api/tests.py b/opennebula_api/tests.py
index bd744a66..0d6ed4eb 100644
--- a/opennebula_api/tests.py
+++ b/opennebula_api/tests.py
@@ -81,6 +81,23 @@ class OpenNebulaManagerTestCases(TestCase):
 
         self.assertEqual(user_public_key, public_key)
         
+    def test_append_public_key_to_user(self):
+        """ Test the manager can append a new public key to an user """
+        self.manager.create_user(self.user)
+        user = self.manager._get_user(self.user)
+        public_key = 'test'
+        self.manager.add_public_key(self.user, public_key)
+        # Fetch new user information from opennebula
+        user.info()
+        old_public_key = user.template.ssh_public_key
+        self.manager.add_public_key(self.user, public_key, merge=True)
+        user.info()
+        new_public_key = user.template.ssh_public_key
+        # Cleanup 
+        user.delete()
+
+        self.assertEqual(new_public_key, '{}\n{}'.format(old_public_key,
+            public_key))
 
     def test_requires_ssh_key_for_new_vm(self):
         """Test the opennebula manager requires the user to have a ssh key when

From 06f372e56d4552bbf80367bcac09e73e2a304529 Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Thu, 1 Jun 2017 22:25:10 +0200
Subject: [PATCH 05/10] Fix tests, add remove_public_key

The tests for VitualMachineSerializer were wrong. The manager now
contains a function to remove public keys from a user
---
 opennebula_api/models.py | 42 +++++++++++++++++++++++++++++++++
 opennebula_api/tests.py  | 51 ++++++++++------------------------------
 2 files changed, 54 insertions(+), 39 deletions(-)

diff --git a/opennebula_api/models.py b/opennebula_api/models.py
index 7872f6fc..4fa5d011 100644
--- a/opennebula_api/models.py
+++ b/opennebula_api/models.py
@@ -479,3 +479,45 @@ class OpenNebulaManager():
 
         except ConnectionError:
             raise
+
+    def remove_public_key(self, user, public_key=''):
+        """ 
+
+        Args: 
+            user (CustomUser): Dynamicweb user 
+            public_key (string): Public key to be removed to the user
+
+        Raises:
+            KeyDoesNotExistsError: If replace is False and the user already has a
+                public key 
+            WrongNameError: If no openebula user with this credentials exists
+            ConnectionError: If the connection to the opennebula server can't be
+                established 
+
+        Returns:
+            True if public_key was removed
+
+        """
+
+        try:
+            open_user = self._get_user(user)
+            try:
+                old_key = open_user.template.ssh_public_key 
+                if public_key not in old_key:
+                    raise KeyDoesNotExistsError()
+                if '\n{}'.format(public_key) in old_key:
+                    public_key = old_key.replace('\n{}'.format(public_key), '')
+                else: 
+                    public_key = old_key.replace(public_key, '')
+
+            except AttributeError:
+                raise KeyDoesNotExistsError()
+                
+            self.oneadmin_client.call('user.update', open_user.id,
+                         '<CONTEXT><SSH_PUBLIC_KEY>{key}</SSH_PUBLIC_KEY></CONTEXT>'.format(key=public_key))
+            return True
+        except WrongNameError:
+            raise
+
+        except ConnectionError:
+            raise
diff --git a/opennebula_api/tests.py b/opennebula_api/tests.py
index 073eb0b9..12b396fe 100644
--- a/opennebula_api/tests.py
+++ b/opennebula_api/tests.py
@@ -5,6 +5,7 @@ import string
 from django.test import TestCase
 
 from .models import OpenNebulaManager
+from .serializers import VirtualMachineSerializer
 from utils.models import CustomUser
 
 class OpenNebulaManagerTestCases(TestCase):
@@ -105,6 +106,8 @@ class OpenNebulaManagerTestCases(TestCase):
         user = self.manager._get_user(self.user)
         public_key = 'test'
         self.manager.add_public_key(self.user, public_key)
+        self.manager.add_public_key(self.user, public_key, merge=True)
+        user.info()
         old_public_key = user.template.ssh_public_key
         self.manager.remove_public_key(self.user, public_key)
         user.info()
@@ -112,7 +115,8 @@ class OpenNebulaManagerTestCases(TestCase):
         # Cleanup 
         user.delete()
 
-        self.assertEqual(new_public_key, old_public_key.replace(public_key, ''))
+        self.assertEqual(new_public_key,
+                old_public_key.replace('{}\n'.format(public_key), '', 1))
 
 
     def test_requires_ssh_key_for_new_vm(self):
@@ -120,49 +124,18 @@ class OpenNebulaManagerTestCases(TestCase):
         creating a new vm"""
 
 
-class VirtualMachineTestCase(TestCase):
+class VirtualMachineSerializerTestCase(TestCase):
     def setUp(self):
         """Define the test client and other test variables."""
-        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)
+        self.manager = OpenNebulaManager(email=None, password=None)
                                            
-        self.virtualmachine = VirtualMachine(opennebula_id=self.opennebula_id,
-                                             template=self.template)
 
     def test_serializer_strips_of_public(self):
-        """ Test the serialized object contains no 'public-'.""" 
+        """ Test the serialized virtual machine object contains no 'public-'.""" 
 
-        template = self.manager.get_templates().first()
-        serialized = VirtualMachineTemplateSerializer(template)
-        self.assertEqual(serialized.data.name, template.name.strip('public-'))
-
-
-        
-    def test_model_can_create_a_virtualmachine(self):
-        """Test the virtualmachine model can create a virtualmachine."""
-        old_count = VirtualMachine.objects.count()
-        self.virtualmachine.save()
-        new_count = VirtualMachine.objects.count()
-        self.assertNotEqual(old_count, new_count)
+        for vm in self.manager.get_vms():
+            serialized = VirtualMachineSerializer(vm)
+            self.assertEqual(serialized.data.get('name'), vm.name.strip('public-'))
+            break
 
 

From ef0589f691654cbbb29ffb359fdd286dfc8257fe Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Thu, 1 Jun 2017 23:06:29 +0200
Subject: [PATCH 06/10] Add german translation

---
 hosting/forms.py                        |  13 ---
 hosting/locale/de/LC_MESSAGES/django.po | 101 +++++++++++++-----------
 2 files changed, 53 insertions(+), 61 deletions(-)

diff --git a/hosting/forms.py b/hosting/forms.py
index d309c983..1c7f7e88 100644
--- a/hosting/forms.py
+++ b/hosting/forms.py
@@ -68,8 +68,6 @@ class UserHostingKeyForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         self.request = kwargs.pop("request")
         super(UserHostingKeyForm, self).__init__(*args, **kwargs)
-        # self.initial['user'].initial = self.request.user.id
-        # print(self.fields)
 
     def clean_name(self):
         return self.data.get('name')
@@ -92,14 +90,3 @@ class UserHostingKeyForm(forms.ModelForm):
     class Meta:
         model = UserHostingKey
         fields = ['user', 'name', 'public_key']
-        labels = {
-            'public_key': _('Writer'),
-        }
-        help_texts = {
-            'public_key': 'Put your shit here',
-        }
-        error_messages = {
-            'name': {
-                'max_length': _("This writer's name is too long."),
-            },
-        }
diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po
index f40ad38c..fbf50c05 100644
--- a/hosting/locale/de/LC_MESSAGES/django.po
+++ b/hosting/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-26 13:35+0000\n"
+"POT-Creation-Date: 2017-06-01 21:03+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,61 +18,64 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: hosting/templates/hosting/base_short.html:67
+#: hosting/forms.py:63
+msgid "Paste here your public key"
+msgstr "Fügen Sie Ihren public key ein"
+
+#: hosting/templates/hosting/base_short.html:68
+#: hosting/templates/hosting/base_short.html:139
 msgid "My Virtual Machines"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:72
+#: hosting/templates/hosting/base_short.html:73
+#: hosting/templates/hosting/base_short.html:145
 #: hosting/templates/hosting/orders.html:12
 msgid "My Orders"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:77
+#: hosting/templates/hosting/base_short.html:78
+#: hosting/templates/hosting/base_short.html:152
 msgid "Keys"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:82
+#: hosting/templates/hosting/base_short.html:83
+#: hosting/templates/hosting/base_short.html:158
 msgid "Notifications "
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:89
+#: hosting/templates/hosting/base_short.html:90
 msgid "Logout"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:94
-#: hosting/templates/hosting/base_short.html:136
+#: hosting/templates/hosting/base_short.html:95
 msgid "How it works"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:97
-#: hosting/templates/hosting/base_short.html:139
+#: hosting/templates/hosting/base_short.html:98
 msgid "Your infrastructure"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:100
-#: hosting/templates/hosting/base_short.html:142
+#: hosting/templates/hosting/base_short.html:101
 msgid "Our inftrastructure"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:103
-#: hosting/templates/hosting/base_short.html:145
+#: hosting/templates/hosting/base_short.html:104
 msgid "Pricing"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:106
-#: hosting/templates/hosting/base_short.html:149
+#: hosting/templates/hosting/base_short.html:107
 msgid "Contact"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:109
-#: hosting/templates/hosting/login.html:29
-#: hosting/templates/hosting/login.html:38
-#: hosting/templates/hosting/reset_password.html:24
-#: hosting/templates/hosting/signup.html:23
+#: hosting/templates/hosting/base_short.html:110
+#: hosting/templates/hosting/login.html:32
+#: hosting/templates/hosting/login.html:41
+#: hosting/templates/hosting/reset_password.html:31
+#: hosting/templates/hosting/signup.html:30
 msgid "Login"
 msgstr ""
 
-#: hosting/templates/hosting/base_short.html:132
+#: hosting/templates/hosting/base_short.html:134
 msgid "Home"
 msgstr ""
 
@@ -144,7 +147,7 @@ msgid "Customers"
 msgstr ""
 
 #: hosting/templates/hosting/bills.html:16
-#: hosting/templates/hosting/virtual_machine_key.html:45
+#: hosting/templates/hosting/virtual_machine_key.html:42
 msgid "Name"
 msgstr ""
 
@@ -173,13 +176,13 @@ msgid "Set your new password"
 msgstr ""
 
 #: hosting/templates/hosting/confirm_reset_password.html:28
-#: hosting/templates/hosting/reset_password.html:20
+#: hosting/templates/hosting/reset_password.html:22
 msgid "Reset"
 msgstr ""
 
 #: hosting/templates/hosting/confirm_reset_password.html:32
-#: hosting/templates/hosting/reset_password.html:24
-#: hosting/templates/hosting/signup.html:23
+#: hosting/templates/hosting/reset_password.html:28
+#: hosting/templates/hosting/signup.html:27
 msgid "Already have an account ?"
 msgstr ""
 
@@ -219,21 +222,27 @@ msgstr ""
 msgid "The %(site_name)s team"
 msgstr ""
 
-#: hosting/templates/hosting/login.html:22
+#: hosting/templates/hosting/login.html:10
+#: hosting/templates/hosting/reset_password.html:10
+#: hosting/templates/hosting/signup.html:9
+msgid "Your VM hosted in Switzerland"
+msgstr ""
+
+#: hosting/templates/hosting/login.html:26
 msgid "You haven been logged out"
 msgstr ""
 
-#: hosting/templates/hosting/login.html:44
+#: hosting/templates/hosting/login.html:49
 msgid "Don't have an account yet ? "
 msgstr ""
 
-#: hosting/templates/hosting/login.html:44
-#: hosting/templates/hosting/signup.html:10
-#: hosting/templates/hosting/signup.html:19
+#: hosting/templates/hosting/login.html:52
+#: hosting/templates/hosting/signup.html:13
+#: hosting/templates/hosting/signup.html:21
 msgid "Sign up"
 msgstr ""
 
-#: hosting/templates/hosting/login.html:46
+#: hosting/templates/hosting/login.html:54
 msgid "Forgot your password ? "
 msgstr ""
 
@@ -310,7 +319,7 @@ msgstr ""
 
 #: hosting/templates/hosting/orders.html:19
 #: hosting/templates/hosting/virtual_machine_detail.html:30
-#: hosting/templates/hosting/virtual_machine_key.html:47
+#: hosting/templates/hosting/virtual_machine_key.html:44
 #: hosting/templates/hosting/virtual_machines.html:31
 msgid "Status"
 msgstr ""
@@ -344,7 +353,7 @@ msgstr ""
 msgid "Delete"
 msgstr ""
 
-#: hosting/templates/hosting/reset_password.html:11
+#: hosting/templates/hosting/reset_password.html:14
 msgid "Reset your password"
 msgstr ""
 
@@ -400,50 +409,46 @@ msgstr ""
 msgid "Access Key"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:22
+#: hosting/templates/hosting/virtual_machine_key.html:25
 msgid "Upload your own key. "
 msgstr ""
 
 #: hosting/templates/hosting/virtual_machine_key.html:29
-msgid "Upload Key"
-msgstr ""
-
-#: hosting/templates/hosting/virtual_machine_key.html:33
 msgid "Or generate a new key pair."
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:37
+#: hosting/templates/hosting/virtual_machine_key.html:31
 msgid "Generate Key Pair"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:46
+#: hosting/templates/hosting/virtual_machine_key.html:43
 msgid "Created at"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:68
-#: hosting/templates/hosting/virtual_machine_key.html:81
+#: hosting/templates/hosting/virtual_machine_key.html:66
+#: hosting/templates/hosting/virtual_machine_key.html:79
 msgid "Warning!"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:68
+#: hosting/templates/hosting/virtual_machine_key.html:66
 msgid "You can download your SSH  private key once. Don't lost your key"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:76
+#: hosting/templates/hosting/virtual_machine_key.html:74
 msgid "Copy to Clipboard"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:77
+#: hosting/templates/hosting/virtual_machine_key.html:75
 msgid "Download"
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:81
+#: hosting/templates/hosting/virtual_machine_key.html:79
 msgid ""
 "Your SSH private key was already generated and downloaded, if you lost it, "
 "contact us. "
 msgstr ""
 
-#: hosting/templates/hosting/virtual_machine_key.html:84
+#: hosting/templates/hosting/virtual_machine_key.html:82
 msgid "Generate my key"
 msgstr ""
 

From 715d092b96d64588e80472fbb6b14944dfd5ec7d Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Fri, 2 Jun 2017 00:49:17 +0200
Subject: [PATCH 07/10] Add ssh key to new vm

Create_vm now stes to public key correctly
---
 hosting/views.py              | 43 +++++++++++------------------------
 opennebula_api/models.py      | 30 ++++++++----------------
 opennebula_api/serializers.py |  3 ++-
 3 files changed, 24 insertions(+), 52 deletions(-)

diff --git a/hosting/views.py b/hosting/views.py
index a694373a..3044590b 100644
--- a/hosting/views.py
+++ b/hosting/views.py
@@ -355,17 +355,6 @@ class GenerateVMSSHKeysView(LoginRequiredMixin, FormView):
         return render(self.request, self.template_name, context)
 
     def post(self, request, *args, **kwargs):
-
-       # try:
-       #     UserHostingKey.objects.get(
-       #         user=self.request.user
-       #     )
-       #     return HttpResponseRedirect(reverse('hosting:key_pair'))
-
-       # except UserHostingKey.DoesNotExist:
-       #     pass
-
-        
         form = self.get_form()
         if form.is_valid():
             return self.form_valid(form)
@@ -418,11 +407,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
         return context
 
     def get(self, request, *args, **kwargs):
-        try:
-            UserHostingKey.objects.get(
-                user=self.request.user
-            )
-        except UserHostingKey.DoesNotExist:
+        if not UserHostingKey.objects.filter( user=self.request.user).exists():
             messages.success(
                 request,
                 'In order to create a VM, you create/upload your SSH KEY first.'
@@ -484,14 +469,16 @@ class PaymentVMView(LoginRequiredMixin, FormView):
             manager = OpenNebulaManager(email=owner.email,
                                         password=owner.password)
             # Get user ssh key
-            try:
-                user_key = UserHostingKey.objects.get(
-                    user=self.request.user
-                )
-
-            except UserHostingKey.DoesNotExist:
-                pass
-
+            if not UserHostingKey.objects.filter( user=self.request.user).exists():
+                context.update({
+                    'sshError': 'error',
+                    'form': form
+                })
+                return render(request, self.template_name, context)
+            # For now just get first one
+            user_key = UserHostingKey.objects.filter(
+                    user=self.request.user).first()
+            
             # Create a vm using logged user
             vm_id = manager.create_vm(
                 template_id=vm_template_id,
@@ -636,11 +623,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
 
     def get(self, request, *args, **kwargs):
 
-        try:
-            UserHostingKey.objects.get(
-                user=self.request.user
-            )
-        except UserHostingKey.DoesNotExist:
+        if not UserHostingKey.objects.filter( user=self.request.user).exists():
             messages.success(
                 request,
                 'In order to create a VM, you need to create/upload your SSH KEY first.'
@@ -664,7 +647,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
             )
             context = {
                 'error': 'connection'
-            }
+            }        
 
         return render(request, self.template_name, context)
 
diff --git a/opennebula_api/models.py b/opennebula_api/models.py
index 4fa5d011..5d93c1bd 100644
--- a/opennebula_api/models.py
+++ b/opennebula_api/models.py
@@ -260,7 +260,6 @@ class OpenNebulaManager():
                                   <DEV_PREFIX>vd</DEV_PREFIX>
                                   <IMAGE_ID>{image_id}</IMAGE_ID>
                            </DISK>
-                          </TEMPLATE>
                         """.format(size=1024 * int(specs['disk_size']),
                                    image_id=image_id)
 
@@ -282,10 +281,18 @@ class OpenNebulaManager():
                                   <IMAGE>{image}</IMAGE>
                                   <IMAGE_UNAME>{image_uname}</IMAGE_UNAME>
                            </DISK>
-                          </TEMPLATE>
                         """.format(size=1024 * int(specs['disk_size']),
                                    image=image,
                                    image_uname=image_uname)
+                        
+                                
+        if ssh_key:
+            vm_specs += """<CONTEXT>
+                    <SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>
+                    <NETWORK>YES</NETWORK>
+                   </CONTEXT>
+                              </TEMPLATE>
+                """.format(ssh=public_key)
         vm_id = self.client.call(oca.VmTemplate.METHODS['instantiate'],
                                  template.id,
                                  '',
@@ -293,25 +300,6 @@ class OpenNebulaManager():
                                  vm_specs,
                                  False)
 
-        self.oneadmin_client.call(
-            'vm.update',
-            vm_id,
-            """<CONTEXT>
-                <SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>
-               </CONTEXT>
-            """.format(ssh=ssh_key)
-        )
-        try:
-            self.oneadmin_client.call(
-                oca.VirtualMachine.METHODS['chown'],
-                vm_id,
-                self.opennebula_user.id,
-                self.opennebula_user.group_ids[0]
-            )
-        except AttributeError:
-            logger.info(
-                'Could not change owner for vm with id: {}.'.format(vm_id))
-
         self.oneadmin_client.call(
             oca.VirtualMachine.METHODS['action'],
             'release',
diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py
index 226d9f47..824ae7c6 100644
--- a/opennebula_api/serializers.py
+++ b/opennebula_api/serializers.py
@@ -15,7 +15,6 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
     cores       = serializers.SerializerMethodField() 
     disk_size   = serializers.SerializerMethodField()
     memory      = serializers.SerializerMethodField()
-    price       = serializers.SerializerMethodField()
 
     def get_cores(self, obj):
         if hasattr(obj.template, 'vcpu'):
@@ -39,6 +38,8 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
     def get_name(self, obj):
         return obj.name.strip('public-')
 
+
+
 class VirtualMachineSerializer(serializers.Serializer):
     """Serializer to map the virtual machine instance into JSON format."""
 

From e4291083bd6cb580f8479b2a5b05effcdd37efe7 Mon Sep 17 00:00:00 2001
From: modulos <modulos@protonmail.com>
Date: Fri, 2 Jun 2017 00:58:48 +0200
Subject: [PATCH 08/10] Append changes

---
 Changelog | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/Changelog b/Changelog
index 7df1f113..ae899654 100644
--- a/Changelog
+++ b/Changelog
@@ -5,3 +5,8 @@
     * [datacenterlight] Fix initially shown price
 1.0.2: 2017-05-28
     * [datacenterlight] Fixed login redirecting to blank page after logout
+
+next
+	* [opennebula_api] Improve testing, add ssh key functions
+	* [opennebula_api] Remove template views
+	* [datacenterlight] Allow user to have multiple ssh keys 

From 83705984d069c215fb139d039e6442653d0bec94 Mon Sep 17 00:00:00 2001
From: Levi <levinoelvm@gmail.com>
Date: Fri, 2 Jun 2017 10:36:17 -0500
Subject: [PATCH 09/10] removed more info button

---
 datacenterlight/templates/datacenterlight/index.html | 1 -
 1 file changed, 1 deletion(-)

diff --git a/datacenterlight/templates/datacenterlight/index.html b/datacenterlight/templates/datacenterlight/index.html
index 89297926..f3ea045d 100755
--- a/datacenterlight/templates/datacenterlight/index.html
+++ b/datacenterlight/templates/datacenterlight/index.html
@@ -217,7 +217,6 @@
                 <div class="col-xs-12 col-md-6 text">
                     <h2 class="section-heading">{% trans "We are cutting down the costs significantly!" %}</h2>
                     <p class="lead">{% trans "Affordable VM hosting based in Switzerland" %}</p>
-                     <a href="#" class="btn btn-info btn-lg">{% trans "More Info" %}</a> 
                 </div>
 
                 <div class="col-xs-12 col-md-6 hero-feature">

From cd9c8938ed2ccb63b3c03dedc52dfd7cc67702bf Mon Sep 17 00:00:00 2001
From: Levi <levinoelvm@gmail.com>
Date: Fri, 2 Jun 2017 11:11:04 -0500
Subject: [PATCH 10/10] hidden footbar on mobile devices

---
 Changelog                                 | 2 ++
 hosting/templates/hosting/base_short.html | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/Changelog b/Changelog
index 7df1f113..a309e059 100644
--- a/Changelog
+++ b/Changelog
@@ -5,3 +5,5 @@
     * [datacenterlight] Fix initially shown price
 1.0.2: 2017-05-28
     * [datacenterlight] Fixed login redirecting to blank page after logout
+1.0.3: 2017-06-02
+    * [datacenterlight] Hotfix, remove footer on mobile devices
diff --git a/hosting/templates/hosting/base_short.html b/hosting/templates/hosting/base_short.html
index c6d1772e..3feda88a 100644
--- a/hosting/templates/hosting/base_short.html
+++ b/hosting/templates/hosting/base_short.html
@@ -126,7 +126,7 @@
     <footer class="navbar-fixed-bottom">
         <div class="container">
             <div class="row">
-                <div class="col-lg-12">
+                <div class="col-lg-12 hidden-xs">
                     <ul class="list-inline">
                         <li>
                             <a href="#">{% trans "Home"%}</a>