diff --git a/Changelog b/Changelog index 7df1f113..444c34a6 100644 --- a/Changelog +++ b/Changelog @@ -5,3 +5,10 @@ * [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 + +next + * [opennebula_api] Improve testing, add ssh key functions + * [opennebula_api] Remove template views + * [datacenterlight] Allow user to have multiple ssh keys diff --git a/datacenterlight/static/datacenterlight/css/landing-page.css b/datacenterlight/static/datacenterlight/css/landing-page.css index ad88a73a..50c1739d 100755 --- a/datacenterlight/static/datacenterlight/css/landing-page.css +++ b/datacenterlight/static/datacenterlight/css/landing-page.css @@ -636,24 +636,22 @@ h6 { border-bottom: 1px solid rgba(128, 128, 128, 0.3); position: relative; display: flex; - justify-content: center; + justify-content: space-around; align-items: center; } -.price-calc-section .card .description span{ - font-size: 20px; +.price-calc-section .card .description span { + font-size: 18px; margin-left: 4px; + margin-left: 0px; + /* justify-self: start; */ + width: 30%; + text-align: left; } .price-calc-section .card .description input{ font-size: 20px; text-align: center; - width: 70px; -} -.price-calc-section .card .description #coreValue{ - width: 50px; -} -.price-calc-section .card .description #ramValue{ - width: 50px; + width: 60px; } .price-calc-section .card .description i{ color: #29427A; @@ -797,6 +795,9 @@ h6 { .split-section { padding: 10px 0; } + .split-section .icon-section { + min-height: 160px; + } .split-section .icon-section i{ font-size: 120px; } @@ -836,7 +837,10 @@ h6 { left: 50%; transform: translate(-50%, 0); } - + .contact-section .card .social a { + color: #29427A; + font-size: 30px; + } .price-calc-section{ flex-direction: column; padding: 60px 10px !important; @@ -866,6 +870,12 @@ h6 { text-align: center; } + .price-calc-section .card .description input { + font-size: 17px; + text-align: center; + width: 60px; + } + } @@ -888,8 +898,8 @@ h6 { line-height: 25px; } .price-calc-section .card .description span { - font-size: 17px; - margin-left: 4px; + font-size: 15px; + margin-left: 0px; } } diff --git a/datacenterlight/templates/datacenterlight/index.html b/datacenterlight/templates/datacenterlight/index.html index f3c7a9a0..6650beb0 100755 --- a/datacenterlight/templates/datacenterlight/index.html +++ b/datacenterlight/templates/datacenterlight/index.html @@ -235,7 +235,6 @@

{% trans "We are cutting down the costs significantly!" %}

{% trans "Affordable VM hosting based in Switzerland" %}

- {% trans "More Info" %}
diff --git a/hosting/forms.py b/hosting/forms.py index c94c4822..1c7f7e88 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -4,6 +4,7 @@ from django import forms from membership.models import CustomUser from django.contrib.auth import authenticate +from django.utils.translation import ugettext_lazy as _ from utils.stripe_utils import StripeUtils @@ -57,21 +58,19 @@ class HostingUserSignupForm(forms.ModelForm): class UserHostingKeyForm(forms.ModelForm): - private_key = forms.CharField(widget=forms.PasswordInput(), required=False) - public_key = forms.CharField(widget=forms.PasswordInput(), required=False) - user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(), required=False) - name = forms.CharField(required=False) + private_key = forms.CharField(widget=forms.HiddenInput(), required=False) + public_key = forms.CharField(widget=forms.Textarea(), required=False, + help_text=_('Paste here your public key')) + user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(), + required=False, widget=forms.HiddenInput()) + name = forms.CharField(required=True) 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 "dcl-priv-key-%s" % ( - ''.join(random.choice(string.ascii_lowercase) for i in range(7)) - ) + return self.data.get('name') def clean_user(self): return self.request.user @@ -90,4 +89,4 @@ class UserHostingKeyForm(forms.ModelForm): class Meta: model = UserHostingKey - fields = ['user', 'public_key', 'name'] + fields = ['user', 'name', 'public_key'] diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 9a1fd1a0..1cc7ee01 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-06-01 11:20-0500\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 \n" "Language-Team: LANGUAGE \n" @@ -18,55 +18,64 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: templates/hosting/base_short.html:68 templates/hosting/base_short.html:139 +#: 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 "" -#: templates/hosting/base_short.html:73 templates/hosting/base_short.html:145 -#: templates/hosting/orders.html.py:12 +#: hosting/templates/hosting/base_short.html:73 +#: hosting/templates/hosting/base_short.html:145 +#: hosting/templates/hosting/orders.html:12 msgid "My Orders" msgstr "" -#: templates/hosting/base_short.html:78 templates/hosting/base_short.html:152 +#: hosting/templates/hosting/base_short.html:78 +#: hosting/templates/hosting/base_short.html:152 msgid "Keys" msgstr "" -#: templates/hosting/base_short.html:83 templates/hosting/base_short.html:158 +#: hosting/templates/hosting/base_short.html:83 +#: hosting/templates/hosting/base_short.html:158 msgid "Notifications " msgstr "" -#: templates/hosting/base_short.html:90 +#: hosting/templates/hosting/base_short.html:90 msgid "Logout" msgstr "" -#: templates/hosting/base_short.html:95 +#: hosting/templates/hosting/base_short.html:95 msgid "How it works" msgstr "" -#: templates/hosting/base_short.html:98 +#: hosting/templates/hosting/base_short.html:98 msgid "Your infrastructure" msgstr "" -#: templates/hosting/base_short.html:101 +#: hosting/templates/hosting/base_short.html:101 msgid "Our inftrastructure" msgstr "" -#: templates/hosting/base_short.html:104 +#: hosting/templates/hosting/base_short.html:104 msgid "Pricing" msgstr "" -#: templates/hosting/base_short.html:107 +#: hosting/templates/hosting/base_short.html:107 msgid "Contact" msgstr "" -#: templates/hosting/base_short.html:110 -#: templates/hosting/confirm_reset_password.html:38 -#: templates/hosting/login.html:31 templates/hosting/login.html.py:40 -#: templates/hosting/reset_password.html:30 templates/hosting/signup.html:29 +#: 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 "" -#: templates/hosting/base_short.html:134 +#: hosting/templates/hosting/base_short.html:134 msgid "Home" msgstr "" @@ -135,8 +144,8 @@ msgstr "" msgid "Customers" msgstr "" -#: templates/hosting/bills.html:16 -#: templates/hosting/virtual_machine_key.html:45 +#: hosting/templates/hosting/bills.html:16 +#: hosting/templates/hosting/virtual_machine_key.html:42 msgid "Name" msgstr "" @@ -168,13 +177,14 @@ msgstr "Ihre VM in der Schweiz" msgid "Set your new password" msgstr "" -#: templates/hosting/confirm_reset_password.html:29 -#: templates/hosting/reset_password.html:21 +#: hosting/templates/hosting/confirm_reset_password.html:28 +#: hosting/templates/hosting/reset_password.html:22 msgid "Reset" msgstr "" -#: templates/hosting/confirm_reset_password.html:35 -#: templates/hosting/reset_password.html:27 templates/hosting/signup.html:26 +#: hosting/templates/hosting/confirm_reset_password.html:32 +#: hosting/templates/hosting/reset_password.html:28 +#: hosting/templates/hosting/signup.html:27 msgid "Already have an account ?" msgstr "" @@ -210,20 +220,27 @@ msgstr "" msgid "The %(site_name)s team" msgstr "" -#: templates/hosting/login.html:25 +#: 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 "" -#: templates/hosting/login.html:48 +#: hosting/templates/hosting/login.html:49 msgid "Don't have an account yet ? " msgstr "" -#: templates/hosting/login.html:51 templates/hosting/signup.html.py:12 -#: templates/hosting/signup.html:20 +#: hosting/templates/hosting/login.html:52 +#: hosting/templates/hosting/signup.html:13 +#: hosting/templates/hosting/signup.html:21 msgid "Sign up" msgstr "" -#: templates/hosting/login.html:53 +#: hosting/templates/hosting/login.html:54 msgid "Forgot your password ? " msgstr "" @@ -298,10 +315,10 @@ msgstr "" msgid "Amount" msgstr "" -#: templates/hosting/orders.html:19 -#: templates/hosting/virtual_machine_detail.html:30 -#: templates/hosting/virtual_machine_key.html:47 -#: templates/hosting/virtual_machines.html:31 +#: hosting/templates/hosting/orders.html:19 +#: hosting/templates/hosting/virtual_machine_detail.html:30 +#: hosting/templates/hosting/virtual_machine_key.html:44 +#: hosting/templates/hosting/virtual_machines.html:31 msgid "Status" msgstr "" @@ -333,7 +350,7 @@ msgstr "" msgid "Delete" msgstr "" -#: templates/hosting/reset_password.html:13 +#: hosting/templates/hosting/reset_password.html:14 msgid "Reset your password" msgstr "" @@ -389,50 +406,46 @@ msgstr "" msgid "Access Key" msgstr "" -#: templates/hosting/virtual_machine_key.html:22 +#: hosting/templates/hosting/virtual_machine_key.html:25 msgid "Upload your own key. " msgstr "" -#: templates/hosting/virtual_machine_key.html:29 -msgid "Upload Key" -msgstr "" - -#: templates/hosting/virtual_machine_key.html:33 +#: hosting/templates/hosting/virtual_machine_key.html:29 msgid "Or generate a new key pair." msgstr "" -#: templates/hosting/virtual_machine_key.html:37 +#: hosting/templates/hosting/virtual_machine_key.html:31 msgid "Generate Key Pair" msgstr "" -#: templates/hosting/virtual_machine_key.html:46 +#: hosting/templates/hosting/virtual_machine_key.html:43 msgid "Created at" msgstr "" -#: templates/hosting/virtual_machine_key.html:68 -#: 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 "" -#: 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 "" -#: templates/hosting/virtual_machine_key.html:76 +#: hosting/templates/hosting/virtual_machine_key.html:74 msgid "Copy to Clipboard" msgstr "" -#: templates/hosting/virtual_machine_key.html:77 +#: hosting/templates/hosting/virtual_machine_key.html:75 msgid "Download" msgstr "" -#: 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 "" -#: templates/hosting/virtual_machine_key.html:84 +#: hosting/templates/hosting/virtual_machine_key.html:82 msgid "Generate my key" msgstr "" diff --git a/hosting/static/hosting/css/commons.css b/hosting/static/hosting/css/commons.css index 6b2fcaf9..15020543 100644 --- a/hosting/static/hosting/css/commons.css +++ b/hosting/static/hosting/css/commons.css @@ -50,5 +50,8 @@ select { width: 280px; } + .content-dashboard { + width: 90%; + } } diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index c7f85b72..d8812ec7 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -344,6 +344,19 @@ h6 { ul.banner-social-buttons > li:last-child { margin-bottom: 0; } + .auth-box .form { + padding: 15px 0px 0 0; + } + .auth-box.sign-up .form { + padding: 15px 0px 0 0; + } + .auth-box .form .form-control { + height: 44px; + font-size: 13px; + } + .auth-container .auth-title { + display: none; + } } @media (max-width: 540px) { .auth-container .auth-title h2{ @@ -357,7 +370,6 @@ h6 { margin-bottom: 50px; } .auth-box .form { - padding: 15px; width: 90%; } .auth-box .section-heading { diff --git a/hosting/templates/hosting/base_short.html b/hosting/templates/hosting/base_short.html index 26bcac0c..53625134 100644 --- a/hosting/templates/hosting/base_short.html +++ b/hosting/templates/hosting/base_short.html @@ -73,11 +73,7 @@ {% trans "My Orders"%} -
  • - - {% trans "Keys"%} - -
  • +
  • {% trans "Notifications "%} @@ -88,6 +84,11 @@ {{request.user.name}}
  • - -
    - + {% endif %} diff --git a/hosting/templates/hosting/virtual_machine_key.html b/hosting/templates/hosting/virtual_machine_key.html index 4ef8fb2e..bbac6e4f 100644 --- a/hosting/templates/hosting/virtual_machine_key.html +++ b/hosting/templates/hosting/virtual_machine_key.html @@ -6,7 +6,7 @@
    -
    + {% csrf_token %}

    {% trans "Access Key"%}

    {% if messages %} @@ -15,28 +15,25 @@ {{ message }} {% endfor %}
    - {% endif %} -
    - {% if not user_key %} -

    + {% endif %} + {% for field in form %} + + {% bootstrap_field field %} + {% endfor %} + {% buttons %} +

    -
    - - -
    -
    -
    - -

    - {% trans "Or generate a new key pair."%} - -

    -
    + +
    +
    + {% trans "Or generate a new key pair."%}
    +
    + + {% endbuttons %} +
    - {% else %}
    Use your created key to access to the machine. If you lost it, contact us.

    @@ -49,6 +46,7 @@ + {% for user_key in keys %} @@ -57,9 +55,9 @@ + {% endfor %}
    {{user_key.name}} {{user_key.created_at}}
    - {% endif %} {% if private_key %} diff --git a/hosting/tests.py b/hosting/tests.py index 7ce503c2..7cee782d 100644 --- a/hosting/tests.py +++ b/hosting/tests.py @@ -1,3 +1,5 @@ from django.test import TestCase # Create your tests here. + +test_user_can_add_key() diff --git a/hosting/views.py b/hosting/views.py index f08ad1a4..3044590b 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -301,16 +301,12 @@ class GenerateVMSSHKeysView(LoginRequiredMixin, FormView): self ).get_context_data(**kwargs) - try: - user_key = UserHostingKey.objects.get( - user=self.request.user - ) - - except UserHostingKey.DoesNotExist: - user_key = None + user_keys = UserHostingKey.objects.filter( + user=self.request.user + ) context.update({ - 'user_key': user_key + 'keys': user_keys }) return context @@ -351,24 +347,14 @@ class GenerateVMSSHKeysView(LoginRequiredMixin, FormView): opennebula_user = user_pool.get_by_name(owner.email) # Get user ssh key - user_key = UserHostingKey.objects.get(user=owner) + public_key = form.cleaned_data.get('public_key') # Add ssh key to user manager.oneadmin_client.call('user.update', opennebula_user.id, - '{ssh_key}'.format(ssh_key=user_key.public_key)) + '{key}'.format(key=public_key)) 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) @@ -421,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.' @@ -487,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, @@ -639,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.' @@ -667,7 +647,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): ) context = { 'error': 'connection' - } + } return render(request, self.template_name, context) 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 3eac0b6e..5d93c1bd 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -2,12 +2,15 @@ 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 +from .exceptions import KeyExistsError, UserExistsError, UserCredentialError + logger = logging.getLogger(__name__) @@ -35,10 +38,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 +73,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 +166,7 @@ class OpenNebulaManager(): host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) - raise ConnectionRefusedError + raise return user_pool def _get_vm_pool(self): @@ -171,7 +260,6 @@ class OpenNebulaManager(): vd {image_id} - """.format(size=1024 * int(specs['disk_size']), image_id=image_id) @@ -193,10 +281,18 @@ class OpenNebulaManager(): {image} {image_uname} - """.format(size=1024 * int(specs['disk_size']), image=image, image_uname=image_uname) + + + if ssh_key: + vm_specs += """ + {ssh} + YES + + + """.format(ssh=public_key) vm_id = self.client.call(oca.VmTemplate.METHODS['instantiate'], template.id, '', @@ -204,25 +300,6 @@ class OpenNebulaManager(): vm_specs, False) - self.oneadmin_client.call( - 'vm.update', - vm_id, - """ - {ssh} - - """.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', @@ -350,3 +427,85 @@ class OpenNebulaManager(): self.opennebula_user.id, new_password ) + + def add_public_key(self, user, public_key='', merge=False): + """ + + Args: + user (CustomUser): Dynamicweb user + public_key (string): Public key to add to the user + merge (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 merge: + raise KeyExistsError() + public_key += '\n{key}'.format(key=old_key) + + except AttributeError: + pass + self.oneadmin_client.call('user.update', open_user.id, + '{key}'.format(key=public_key)) + return True + except WrongNameError: + raise + + 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, + '{key}'.format(key=public_key)) + return True + except WrongNameError: + raise + + except ConnectionError: + raise diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py index 4af6c3ff..824ae7c6 100644 --- a/opennebula_api/serializers.py +++ b/opennebula_api/serializers.py @@ -11,36 +11,10 @@ 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'): @@ -58,24 +32,14 @@ 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 def get_name(self, obj): return obj.name.strip('public-') + + class VirtualMachineSerializer(serializers.Serializer): """Serializer to map the virtual machine instance into JSON format.""" diff --git a/opennebula_api/tests.py b/opennebula_api/tests.py index 2dc9884f..12b396fe 100644 --- a/opennebula_api/tests.py +++ b/opennebula_api/tests.py @@ -1,35 +1,54 @@ +import socket +import random +import string + from django.test import TestCase -from .models import VirtualMachine, VirtualMachineTemplate, OpenNebulaManager + +from .models import OpenNebulaManager +from .serializers import VirtualMachineSerializer +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,105 +57,85 @@ 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 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.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_remove_public_key_to_user(self): + """ Test the manager can remove a public key from 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) + 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() + new_public_key = user.template.ssh_public_key + # Cleanup + user.delete() + + self.assertEqual(new_public_key, + old_public_key.replace('{}\n'.format(public_key), '', 1)) + + + 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""" + + +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) - - - 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.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-')) + for vm in self.manager.get_vms(): + serialized = VirtualMachineSerializer(vm) + self.assertEqual(serialized.data.get('name'), vm.name.strip('public-')) + break - - 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_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) 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[0-9]+)/$', TemplateDetailsView.as_view(), - name="templates_details"), - url(r'^vms/$', VmCreateView.as_view(), name="vm_create"), url(r'^vms/(?P[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