diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 234f2a3b..3c7869a1 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/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-09-17 01:31+0530\n" +"POT-Creation-Date: 2017-09-16 14:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#, python-format +msgid "Your New VM %(vm_name)s at Data Center Light" +msgstr "Deine neue VM %(vm_name)s bei Data Center Light" + msgid "Enter name" msgstr "Name" @@ -184,7 +188,7 @@ msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" msgid "Toggle navigation" -msgstr "umschalten" +msgstr "Umschalten" msgid "Why Data Center Light?" msgstr "Warum Data Center Light?" @@ -193,7 +197,7 @@ msgid "Login" msgstr "Anmelden" msgid "Dashboard" -msgstr "Dashboard" +msgstr "" msgid "Finally, an affordable VM hosting in Switzerland!" msgstr "Endlich: bezahlbares VM Hosting in der Schweiz" diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 1e3e1caa..1335869b 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -1,15 +1,22 @@ -from dynamicweb.celery import app +from datetime import datetime + +from celery.exceptions import MaxRetriesExceededError from celery.utils.log import get_task_logger +from celery import current_task from django.conf import settings +from django.core.mail import EmailMessage +from django.utils import translation +from django.utils.translation import ugettext_lazy as _ + +from dynamicweb.celery import app +from hosting.models import HostingOrder, HostingBill +from membership.models import StripeCustomer, CustomUser from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineSerializer -from hosting.models import HostingOrder, HostingBill +from utils.hosting_utils import get_all_public_keys from utils.forms import UserBillingAddressForm -from datetime import datetime -from membership.models import StripeCustomer -from django.core.mail import EmailMessage +from utils.mailer import BaseEmail from utils.models import BillingAddress -from celery.exceptions import MaxRetriesExceededError logger = get_task_logger(__name__) @@ -45,25 +52,36 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, charge, cc_details): + logger.debug("Running create_vm_task on {}".format(current_task.request.hostname)) vm_id = None try: final_price = specs.get('price') billing_address = BillingAddress.objects.filter( id=billing_address_id).first() customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() - # Create OpenNebulaManager - manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, - password=settings.OPENNEBULA_PASSWORD) - # Create a vm using oneadmin, also specify the name + if 'pass' in user: + on_user = user.get('email') + on_pass = user.get('pass') + logger.debug("Using user {user} to create VM".format(user=on_user)) + vm_name = None + else: + on_user = settings.OPENNEBULA_USERNAME + on_pass = settings.OPENNEBULA_PASSWORD + logger.debug("Using OpenNebula admin user to create VM") + vm_name = "{email}-{template_name}-{date}".format( + email=user.get('email'), + template_name=template.get('name'), + date=int(datetime.now().strftime("%s"))) + + # Create OpenNebulaManager + manager = OpenNebulaManager(email=on_user, password=on_pass) + vm_id = manager.create_vm( template_id=vm_template_id, specs=specs, ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY, - vm_name="{email}-{template_name}-{date}".format( - email=user.get('email'), - template_name=template.get('name'), - date=int(datetime.now().strftime("%s"))) + vm_name=vm_name ) if vm_id is None: @@ -122,6 +140,54 @@ def create_vm_task(self, vm_template_id, user, specs, template, } email = EmailMessage(**email_data) email.send() + + if 'pass' in user: + lang = 'en-us' + if user.get('language') is not None: + logger.debug("Language is set to {}".format(user.get('language'))) + lang = user.get('language') + translation.activate(lang) + # Send notification to the user as soon as VM has been booked + context = { + 'vm': vm, + 'order': order, + 'base_url': "{0}://{1}".format(user.get('request_scheme'), + user.get('request_host')), + 'page_header': _( + 'Your New VM %(vm_name)s at Data Center Light') % { + 'vm_name': vm.get('name')} + } + email_data = { + 'subject': context.get('page_header'), + 'to': user.get('email'), + 'context': context, + 'template_name': 'new_booked_vm', + 'template_path': 'hosting/emails/', + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, + } + email = BaseEmail(**email_data) + email.send() + + # try to see if we have the IP and that if the ssh keys can + # be configured + new_host = manager.get_primary_ipv4(vm_id) + logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) + if new_host is not None: + custom_user = CustomUser.objects.get(email=user.get('email')) + if custom_user is not None: + public_keys = get_all_public_keys(custom_user) + keys = [{'value': key, 'state': True} for key in + public_keys] + if len(keys) > 0: + logger.debug( + "Calling configure on {host} for {num_keys} keys".format( + host=new_host, num_keys=len(keys))) + # Let's delay the task by 75 seconds to be sure + # that we run the cdist configure after the host + # is up + manager.manage_public_key(keys, + hosts=[new_host], + countdown=75) except Exception as e: logger.error(str(e)) try: @@ -134,8 +200,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, email_data = { 'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT, msg_text), - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, 'body': ',\n'.join(str(i) for i in self.request.args) } email = EmailMessage(**email_data) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index 7c2f7353..c34c56ba 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -115,8 +115,8 @@ class CeleryTaskTestCase(TestCase): 'response_object').stripe_plan_id}]) stripe_subscription_obj = subscription_result.get('response_object') # Check if the subscription was approved and is active - if stripe_subscription_obj is None or \ - stripe_subscription_obj.status != 'active': + if stripe_subscription_obj is None \ + or stripe_subscription_obj.status != 'active': msg = subscription_result.get('error') raise Exception("Creating subscription failed: {}".format(msg)) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 08ce457d..dc2917ea 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -173,7 +173,8 @@ TEMPLATES = [ os.path.join(PROJECT_DIR, 'nosystemd/templates/'), os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'), - os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'), + os.path.join(PROJECT_DIR, + 'ungleich/templates/cms/ungleichch'), os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'), os.path.join(PROJECT_DIR, 'ungleich_page/templates/ungleich_page'), @@ -559,9 +560,21 @@ CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND') CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' -CELERY_TIMEZONE = 'Europe/Zurich' +# CELERY_TIMEZONE = 'Europe/Zurich' CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) +DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO') + +DCL_ERROR_EMAILS_TO_LIST = [] +if DCL_ERROR_EMAILS_TO is not None: + DCL_ERROR_EMAILS_TO_LIST = [x.strip() for x in + DCL_ERROR_EMAILS_TO.split( + ',')] \ + if "," in DCL_ERROR_EMAILS_TO else [DCL_ERROR_EMAILS_TO.strip()] + +if 'info@ungleich.ch' not in DCL_ERROR_EMAILS_TO_LIST: + DCL_ERROR_EMAILS_TO_LIST.append('info@ungleich.ch') + ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING') if ENABLE_DEBUG_LOGGING: @@ -585,6 +598,9 @@ if ENABLE_DEBUG_LOGGING: }, } +TEST_MANAGE_SSH_KEY_PUBKEY = env('TEST_MANAGE_SSH_KEY_PUBKEY') +TEST_MANAGE_SSH_KEY_HOST = env('TEST_MANAGE_SSH_KEY_HOST') + DEBUG = bool_env('DEBUG') if DEBUG: diff --git a/hosting/forms.py b/hosting/forms.py index 288a8caf..056d0004 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -1,16 +1,22 @@ import datetime +import logging +import subprocess +import tempfile from django import forms -from membership.models import CustomUser from django.contrib.auth import authenticate - from django.utils.translation import ugettext_lazy as _ +from membership.models import CustomUser +from utils.hosting_utils import get_all_public_keys from .models import UserHostingKey +logger = logging.getLogger(__name__) + def generate_ssh_key_name(): - return 'dcl-generated-key-' + datetime.datetime.now().strftime('%m%d%y%H%M') + return 'dcl-generated-key-' + datetime.datetime.now().strftime( + '%m%d%y%H%M') class HostingUserLoginForm(forms.Form): @@ -38,9 +44,7 @@ class HostingUserLoginForm(forms.Form): CustomUser.objects.get(email=email) return email except CustomUser.DoesNotExist: - raise forms.ValidationError("User does not exist") - else: - return email + raise forms.ValidationError(_("User does not exist")) class HostingUserSignupForm(forms.ModelForm): @@ -51,7 +55,8 @@ class HostingUserSignupForm(forms.ModelForm): model = CustomUser fields = ['name', 'email', 'password'] widgets = { - 'name': forms.TextInput(attrs={'placeholder': 'Enter your name or company name'}), + 'name': forms.TextInput( + attrs={'placeholder': 'Enter your name or company name'}), } def clean_confirm_password(self): @@ -65,19 +70,55 @@ class HostingUserSignupForm(forms.ModelForm): class UserHostingKeyForm(forms.ModelForm): private_key = forms.CharField(widget=forms.HiddenInput(), required=False) public_key = forms.CharField(widget=forms.Textarea( - attrs={'class': 'form_public_key', 'placeholder': _('Paste here your public key')}), + attrs={'class': 'form_public_key', + 'placeholder': _('Paste here your public key')}), required=False, ) user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(), - required=False, widget=forms.HiddenInput()) + required=False, + widget=forms.HiddenInput()) name = forms.CharField(required=False, widget=forms.TextInput( - attrs={'class': 'form_key_name', 'placeholder': _('Give a name to your key')})) + attrs={'class': 'form_key_name', + 'placeholder': _('Give a name to your key')})) def __init__(self, *args, **kwargs): self.request = kwargs.pop("request") super(UserHostingKeyForm, self).__init__(*args, **kwargs) self.fields['name'].label = _('Key name') + def clean_public_key(self): + """ + Validates a public ssh key using `ssh-keygen -lf key.pub` + Also checks if a given key already exists in the database and + alerts the user of it. + :return: + """ + if 'generate' in self.request.POST: + return self.data.get('public_key') + KEY_ERROR_MESSAGE = _("Please input a proper SSH key") + openssh_pubkey_str = self.data.get('public_key').strip() + + if openssh_pubkey_str in get_all_public_keys(self.request.user): + key_name = UserHostingKey.objects.filter( + user_id=self.request.user.id, + public_key=openssh_pubkey_str).first().name + KEY_EXISTS_MESSAGE = _( + "This key exists already with the name \"%(name)s\"") % { + 'name': key_name} + raise forms.ValidationError(KEY_EXISTS_MESSAGE) + + with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file: + tmp_public_key_file.write(openssh_pubkey_str.encode('utf-8')) + tmp_public_key_file.flush() + try: + subprocess.check_output( + ['ssh-keygen', '-lf', tmp_public_key_file.name]) + except subprocess.CalledProcessError as cpe: + logger.debug( + "Not a correct ssh format {error}".format(error=str(cpe))) + raise forms.ValidationError(KEY_ERROR_MESSAGE) + return openssh_pubkey_str + def clean_name(self): return self.data.get('name') diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index c7a13e34..006e8b20 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -24,6 +24,9 @@ msgstr "Dein Benutzername und/oder Dein Passwort ist falsch." msgid "Your account is not activated yet." msgstr "Dein Account wurde noch nicht aktiviert." +msgid "User does not exist" +msgstr "Der Benutzer existiert nicht" + msgid "Paste here your public key" msgstr "Füge deinen Public Key ein" @@ -33,6 +36,13 @@ msgstr "Gebe deinem SSH-Key einen Name" msgid "Key name" msgstr "Key-Name" +msgid "Please input a proper SSH key" +msgstr "Bitte verwende einen gültigen SSH-Key" + +#, python-format +msgid "This key exists already with the name \"%(name)s\"" +msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits" + msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" diff --git a/hosting/static/hosting/js/virtual_machine_detail.js b/hosting/static/hosting/js/virtual_machine_detail.js index 303f95ca..b6202e97 100644 --- a/hosting/static/hosting/js/virtual_machine_detail.js +++ b/hosting/static/hosting/js/virtual_machine_detail.js @@ -1,18 +1,48 @@ +$(document).ready(function () { + var create_vm_form = $('#virtual_machine_create_form'); + create_vm_form.submit(function () { + $('#btn-create-vm').prop('disabled', true); + $.ajax({ + url: create_vm_form.attr('action'), + type: 'POST', + data: create_vm_form.serialize(), + success: function (data) { + if (data.status === true) { + fa_icon = $('.modal-icon > .fa'); + fa_icon.attr('class', 'fa fa-check'); + $('.modal-header > .close').attr('class', 'close'); + $('#createvm-modal-title').text(data.msg_title); + $('#createvm-modal-body').text(data.msg_body); + $('#createvm-modal').on('hidden.bs.modal', function () { + window.location = data.redirect; + }) + } + }, + error: function (xmlhttprequest, textstatus, message) { + fa_icon = $('.modal-icon > .fa'); + fa_icon.attr('class', 'fa fa-times'); + $('.modal-header > .close').attr('class', 'close'); + if (typeof(create_vm_error_message) !== 'undefined') { + $('#createvm-modal-title').text(create_vm_error_message); + } + $('#btn-create-vm').prop('disabled', false); + } + }); + return false; + }); -$( document ).ready(function() { + $('#confirm-cancel').on('click', '.btn-ok', function (e) { + $('#virtual_machine_cancel_form').trigger('submit'); + }); - $('#confirm-cancel').on('click', '.btn-ok', function(e) { - $('#virtual_machine_cancel_form').trigger('submit'); - }); + var hash = window.location.hash; + hash && $('ul.nav a[href="' + hash + '"]').tab('show'); - var hash = window.location.hash; - hash && $('ul.nav a[href="' + hash + '"]').tab('show'); + $('.nav-tabs a').click(function (e) { + $(this).tab('show'); + var scrollmem = $('body').scrollTop() || $('html').scrollTop(); + window.location.hash = this.hash; + $('html,body').scrollTop(scrollmem); + }); - $('.nav-tabs a').click(function (e) { - $(this).tab('show'); - var scrollmem = $('body').scrollTop() || $('html').scrollTop(); - window.location.hash = this.hash; - $('html,body').scrollTop(scrollmem); - }); - -}); \ No newline at end of file +}); diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 64c0b5d3..39c59808 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -1,63 +1,100 @@ {% extends "hosting/base_short.html" %} {% load staticfiles bootstrap3 %} {% load i18n %} +{% load custom_tags %} + {% block content %}
- {% if messages %} + {% if messages %}

-
- {% for message in messages %} - {{ message }} - {% endfor %} -
+
+ {% for message in messages %} + {{ message }} + {% endfor %} +
{% endif %} {% if not error %}
-
-

{{page_header_text}}

{% trans "Order #"%} {{order.id}}

-
-
-
-
+
+

{{page_header_text}}

+

+ {% if order %} + {% trans "Order #"%} {{order.id}} + {% endif %} +

+
+
+
+
{% trans "Date"%}:
- {{order.created_at|date:'Y-m-d H:i'}}

+ + {% if order %} + {{order.created_at|date:'Y-m-d H:i'}} + {% else %} + {% now "Y-m-d H:i" %} + {% endif %} +

+ {% if order %} {% trans "Status:"%}
{% if order.status == 'Approved' %} - {% trans "Approved" %} + + {% trans "Approved" %} + {% else %} - {% trans "Declined" %} + + {% trans "Declined" %} + {% endif %}

+ {% endif %}
-
-
-

{% trans "Billed To:"%}

- {{user.name}}
+
+
+

{% trans "Billed To:"%}

+ {% if order %} + {{user.name}}
{{order.billing_address.street_address}},{{order.billing_address.postal_code}}
- {{order.billing_address.city}}, {{order.billing_address.country}}. -
-
+ {{order.billing_address.city}}, + {{order.billing_address.country}}. + {% else %} + {% with request.session.billing_address_data as billing_address %} + {{billing_address|get_value_from_dict:'cardholder_name'}}
+ {{billing_address|get_value_from_dict:'street_address'}}, + {{billing_address|get_value_from_dict:'postal_code'}}
+ {{billing_address|get_value_from_dict:'city'}}, + {{billing_address|get_value_from_dict:'country'}}. + {% endwith %} + {% endif %} +
+
-
-
-
-
- {% trans "Payment Method:"%}
- {{order.cc_brand}} {% trans "ending in" %} **** {{order.last4}}
- {{user.email}} -
-
-
-
+
+
+
+
+ {% trans "Payment Method:"%}
+ {% if order %} + {{order.cc_brand}} {% trans "ending in" %} **** + {{order.last4}}
+ {{user.email}} + {% else %} + {{cc_brand}} {% trans "ending in" %} **** + {{cc_last4}}
+ {{request.session.user.email}} + {% endif %} +
+
+
+
@@ -65,33 +102,113 @@

{% trans "Order summary"%}


-

{% trans "Cores"%} {{vm.cores}}

+ {% if request.session.specs %} + {% with request.session.specs as vm %} +

{% trans "Cores"%} + {{vm.cpu}} +


-

{% trans "Memory"%} {{vm.memory}} GB

+

{% trans "Memory"%} + {{vm.memory}} GB +


-

{% trans "Disk space"%} {{vm.disk_size}} GB

+

{% trans "Disk space"%} + {{vm.disk_size}} GB +


-

{% trans "Total"%}

{{vm.price}} CHF

+

{% trans "Configuration"%} + {{request.session.template.name}} +

+
+

{% trans "Total"%} +

+ {{vm.price}} CHF + /{% trans "Month" %} + +

+

+ {% endwith %} + {% else %} +

{% trans "Cores"%} + {{vm.cores}} +

+
+

{% trans "Memory"%} + {{vm.memory}} GB +

+
+

{% trans "Disk space"%} + {{vm.disk_size}} GB +

+
+

{% trans "Total"%}

{{vm.price}} + CHF /{% trans "Month" %} +

+ {% endif %}

- {% url 'hosting:payment' as payment_url %} - {% if payment_url in request.META.HTTP_REFERER %} - + {% if not order %} +
+ {% csrf_token %} +
+
+

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.

+
+
+ +
+
+
{% endif %}
{% endif %} + + +