diff --git a/Changelog b/Changelog index cfefdd01..63c59e11 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,7 @@ +2.6.1: 2019-07-09 + * #6941: [hosting dashboard] Show the card's expiry year & month too in the list of added cards (MR!710) +2.6: 2019-07-03 + * #5509: Getting rid of our key by still supporting multiple user keys (MR!709) 2.5.11: 2019-06-11 * #6672: [api] Check VM belongs to user in the infrastructure directly (MR!707) * #bugfix: DE translation fix "Learn mehr" -> "Lerne mehr" (MR!708) diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index d43e91ea..b5ff3ca5 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: 2018-09-26 20:44+0000\n" +"POT-Creation-Date: 2019-07-03 11:18+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" @@ -26,6 +26,22 @@ msgstr "" msgid "Your New VM %(vm_name)s at Data Center Light" msgstr "Deine neue VM %(vm_name)s bei Data Center Light" +msgid "Your VM is almost ready!" +msgstr "Deine VM ist fast fertig!" + +msgid "" +"You need to specify your public SSH key to access your VM. You can either " +"add your existing key, or generate a new key pair by clicking the generate " +"button below. After choosing your public SSH key option you’ll be directed " +"to the order confirmation page." +msgstr "" +"Du musst deinen öffentlichen SSH-Schlüssel angeben, um auf deine VM " +"zugreifen zu können. Du kannst entweder deinen vorhandenen Schlüssel " +"hinzufügen oder ein neues Schlüsselpaar generieren, indem du auf die " +"Schaltfläche \"Generieren\" unten klickst. Nachdem du deine öffentliche SSH-" +"Schlüsseloption ausgewählt hast, wirst du zur Bestellbestätigungsseite " +"weitergeleitet. " + msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" @@ -134,6 +150,10 @@ msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!" msgid "ORDER VM" msgstr "VM BESTELLEN" +#, python-format +msgid "Please enter a value in range %(min_ram)s - 200." +msgstr "Bitte gib einen Wert von %(min_ram)s bis 200 ein." + msgid "VM hosting" msgstr "" @@ -152,9 +172,6 @@ msgstr "Standort: Schweiz" msgid "Please enter a value in range 1 - 48." msgstr "Bitte gib einen Wert von 1 bis 48 ein." -msgid "Please enter a value in range 1 - 200." -msgstr "Bitte gib einen Wert von 1 bis 200 ein." - msgid "Please enter a value in range 10 - 2000." msgstr "Bitte gib einen Wert von 10 bis 2000 ein." @@ -413,6 +430,10 @@ msgstr "Zwischensumme" msgid "VAT" msgstr "Mehrwertsteuer" +#, fuzzy, python-format +#| msgid "" +#| "By clicking \"Place order\" this plan will charge your credit card " +#| "account with %(total_price)s CHF/month" msgid "" "By clicking \"Place order\" this plan will charge your credit card account " "with %(total_price)s CHF/month" @@ -420,6 +441,10 @@ msgstr "" "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " "%(vm_total_price)s CHF pro Monat belastet" +#, fuzzy, python-format +#| msgid "" +#| "By clicking \"Place order\" this payment will charge your credit card " +#| "account with a one time amount of %(total_price)s CHF" msgid "" "By clicking \"Place order\" this payment will charge your credit card " "account with a one time amount of %(total_price)s CHF" @@ -535,6 +560,9 @@ msgstr "Tagen sagen mehr als Worte – Teste jetzt unsere VM!" msgid "Invalid number of cores" msgstr "Ungültige Anzahle CPU-Kerne" +msgid "Invalid calculator properties" +msgstr "" + msgid "Invalid RAM size" msgstr "Ungültige RAM-Grösse" diff --git a/datacenterlight/static/datacenterlight/css/common.css b/datacenterlight/static/datacenterlight/css/common.css index 00ee52cc..b19b5852 100644 --- a/datacenterlight/static/datacenterlight/css/common.css +++ b/datacenterlight/static/datacenterlight/css/common.css @@ -186,3 +186,8 @@ footer .dcl-link-separator::before { background: transparent !important; resize: none; } + +.existing-keys-title { + font-weight: bold; + font-size: 14px; +} diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 5f12b7df..8b4626e8 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -8,7 +8,6 @@ from django.core.mail import EmailMessage from django.core.urlresolvers import reverse from django.utils import translation from django.utils.translation import ugettext_lazy as _ -from time import sleep from dynamicweb.celery import app from hosting.models import HostingOrder @@ -16,7 +15,7 @@ from membership.models import CustomUser from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineSerializer from utils.hosting_utils import ( - get_all_public_keys, get_or_create_vm_detail, ping_ok + get_all_public_keys, get_or_create_vm_detail ) from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils @@ -79,10 +78,14 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id): # Create OpenNebulaManager manager = OpenNebulaManager(email=on_user, password=on_pass) + custom_user = CustomUser.objects.get(email=user.get('email')) + pub_keys = get_all_public_keys(custom_user) + if manager.email != settings.OPENNEBULA_USERNAME: + manager.save_key_in_opennebula_user('\n'.join(pub_keys)) vm_id = manager.create_vm( template_id=vm_template_id, specs=specs, - ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY, + ssh_key='\n'.join(pub_keys), vm_name=vm_name ) @@ -188,65 +191,9 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id): email = BaseEmail(**email_data) email.send() - # try to see if we have the IPv6 of the new vm and that if the ssh - # keys can be configured - vm_ipv6 = manager.get_ipv6(vm_id) logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) - if vm_ipv6 is not None: - custom_user = CustomUser.objects.get(email=user.get('email')) + if vm_id > 0: get_or_create_vm_detail(custom_user, manager, vm_id) - 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=vm_ipv6, num_keys=len(keys) - ) - ) - # Let's wait until the IP responds to ping before we - # run the cdist configure on the host - did_manage_public_key = False - for i in range(0, 15): - if ping_ok(vm_ipv6): - logger.debug( - "{} is pingable. Doing a " - "manage_public_key".format(vm_ipv6) - ) - sleep(10) - manager.manage_public_key( - keys, hosts=[vm_ipv6] - ) - did_manage_public_key = True - break - else: - logger.debug( - "Can't ping {}. Wait 5 secs".format( - vm_ipv6 - ) - ) - sleep(5) - if not did_manage_public_key: - emsg = ("Waited for over 75 seconds for {} to be " - "pingable. But the VM was not reachable. " - "So, gave up manage_public_key. Please do " - "this manually".format(vm_ipv6)) - logger.error(emsg) - email_data = { - 'subject': '{} CELERY TASK INCOMPLETE: {} not ' - 'pingable for 75 seconds'.format( - settings.DCL_TEXT, vm_ipv6 - ), - 'from_email': current_task.request.hostname, - 'to': settings.DCL_ERROR_EMAILS_TO_LIST, - 'body': emsg - } - email = EmailMessage(**email_data) - email.send() - else: - logger.debug("VM's ipv6 is None. Hence not created VMDetail") except Exception as e: logger.error(str(e)) try: diff --git a/datacenterlight/templates/datacenterlight/add_ssh_key.html b/datacenterlight/templates/datacenterlight/add_ssh_key.html new file mode 100644 index 00000000..44048bad --- /dev/null +++ b/datacenterlight/templates/datacenterlight/add_ssh_key.html @@ -0,0 +1,10 @@ + +{% load staticfiles bootstrap3 i18n custom_tags humanize %} + +{% block content %} + {% block userkey_form %} + {% with form_title=_("Your VM is almost ready!") form_sub_title=_("You need to specify your public SSH key to access your VM. You can either add your existing key, or generate a new key pair by clicking the generate button below. After choosing your public SSH key option you’ll be directed to the order confirmation page.") %} + {% include 'hosting/user_key.html' with title=form_title sub_title=form_sub_title %} + {% endwith %} + {% endblock userkey_form %} +{%endblock%} \ No newline at end of file diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index fb6d51b0..4e71eab9 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -131,6 +131,7 @@
{% trans "Credit Card" %}
{% trans "Last" %} 4: ***** {{card.last4}}
{% trans "Type" %}: {{card.brand}}
+
{% trans "Expiry" %}: {{card.exp_month}}/{{card.exp_year}}
{% trans "SELECT" %} diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 31933e12..8a444bef 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -41,6 +41,7 @@

{% trans "Payment method" %}:

{{cc_brand|default:_('Credit Card')}} {% trans "ending in" %} ****{{cc_last4}}
+ {% trans "Expiry" %} {{cc_exp_year}}/{{cc_exp_month}}
{{request.user.email}}

diff --git a/datacenterlight/urls.py b/datacenterlight/urls.py index 006e7fc3..13296de7 100644 --- a/datacenterlight/urls.py +++ b/datacenterlight/urls.py @@ -1,12 +1,12 @@ from django.conf.urls import url from django.views.generic import TemplateView, RedirectView +from utils.views import AskSSHKeyView from .views import ( IndexView, PaymentOrderView, OrderConfirmationView, WhyDataCenterLightView, ContactUsView ) - urlpatterns = [ url(r'^$', IndexView.as_view(), name='index'), url(r'^t/$', IndexView.as_view(), name='index_t'), @@ -20,6 +20,8 @@ urlpatterns = [ url(r'^payment/?$', PaymentOrderView.as_view(), name='payment'), url(r'^order-confirmation/?$', OrderConfirmationView.as_view(), name='order_confirmation'), + url(r'^add-ssh-key/?$', AskSSHKeyView.as_view(), + name='add_ssh_key'), url(r'^contact/?$', ContactUsView.as_view(), name='contact_us'), url(r'glasfaser/?$', TemplateView.as_view(template_name='ungleich_page/glasfaser.html'), diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index 208d39f3..11d2b82e 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -1,8 +1,9 @@ import logging + import pyotp import requests -from django.contrib.sites.models import Site from django.conf import settings +from django.contrib.sites.models import Site from datacenterlight.tasks import create_vm_task from hosting.models import HostingOrder, HostingBill, OrderDetail @@ -99,7 +100,8 @@ def clear_all_session_vars(request): for session_var in ['specs', 'template', 'billing_address', 'billing_address_data', 'card_id', 'token', 'customer', 'generic_payment_type', - 'generic_payment_details', 'product_id']: + 'generic_payment_details', 'product_id', + 'order_confirm_url', 'new_user_hosting_key_id']: if session_var in request.session: del request.session[session_var] diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 5dc3a3d3..ae649623 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -13,18 +13,20 @@ from django.views.decorators.cache import cache_control from django.views.generic import FormView, CreateView, DetailView from hosting.forms import ( - HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm + HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm, + UserHostingKeyForm ) from hosting.models import ( - HostingBill, HostingOrder, UserCardDetail, GenericProduct + HostingBill, HostingOrder, UserCardDetail, GenericProduct, UserHostingKey ) from membership.models import CustomUser, StripeCustomer +from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VMTemplateSerializer from utils.forms import ( BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm, BillingAddress ) -from utils.hosting_utils import get_vm_price_with_vat +from utils.hosting_utils import get_vm_price_with_vat, get_all_public_keys from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task from .cms_models import DCLCalculatorPluginModel @@ -521,20 +523,34 @@ class PaymentOrderView(FormView): request.session['customer'] = customer.stripe_id else: request.session['customer'] = customer - return HttpResponseRedirect( - reverse('datacenterlight:order_confirmation')) + + # For generic payment we take the user directly to confirmation + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + return HttpResponseRedirect( + reverse('datacenterlight:order_confirmation')) + else: + self.request.session['order_confirm_url'] = reverse('datacenterlight:order_confirmation') + return HttpResponseRedirect( + reverse('datacenterlight:add_ssh_key')) else: context = self.get_context_data() context['billing_address_form'] = address_form return self.render_to_response(context) -class OrderConfirmationView(DetailView): +class OrderConfirmationView(DetailView, FormView): + form_class = UserHostingKeyForm template_name = "datacenterlight/order_detail.html" payment_template_name = 'datacenterlight/landing_payment.html' context_object_name = "order" model = HostingOrder + def get_form_kwargs(self): + kwargs = super(OrderConfirmationView, self).get_form_kwargs() + kwargs.update({'request': self.request}) + return kwargs + @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): context = {} @@ -552,11 +568,15 @@ class OrderConfirmationView(DetailView): card_details_response = card_details['response_object'] context['cc_last4'] = card_details_response['last4'] context['cc_brand'] = card_details_response['brand'] + context['cc_exp_year'] = card_details_response['exp_year'] + context['cc_exp_month'] = '{:02d}'.format(card_details_response['exp_month']) else: card_id = self.request.session.get('card_id') card_detail = UserCardDetail.objects.get(id=card_id) context['cc_last4'] = card_detail.last4 context['cc_brand'] = card_detail.brand + context['cc_exp_year'] = card_detail.exp_year + context['cc_exp_month'] ='{:02d}'.format(card_detail.exp_month) if ('generic_payment_type' in request.session and self.request.session['generic_payment_type'] == 'generic'): @@ -567,6 +587,8 @@ class OrderConfirmationView(DetailView): else: context.update({ 'vm': request.session.get('specs'), + 'form': UserHostingKeyForm(request=self.request), + 'keys': get_all_public_keys(self.request.user) }) context.update({ 'site_url': reverse('datacenterlight:index'), @@ -830,6 +852,18 @@ class OrderConfirmationView(DetailView): new_user = authenticate(username=custom_user.email, password=password) login(request, new_user) + if 'new_user_hosting_key_id' in self.request.session: + user_hosting_key = UserHostingKey.objects.get(id=self.request.session['new_user_hosting_key_id']) + user_hosting_key.user = new_user + user_hosting_key.save() + + owner = new_user + manager = OpenNebulaManager( + email=owner.email, + password=owner.password + ) + keys_to_save = get_all_public_keys(new_user) + manager.save_key_in_opennebula_user('\n'.join(keys_to_save)) else: # We assume that if the user is here, his/her StripeCustomer # object already exists diff --git a/hosting/forms.py b/hosting/forms.py index 16b06fe0..1bc99b8f 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -1,8 +1,8 @@ import datetime import logging import subprocess - import tempfile + from django import forms from django.conf import settings from django.contrib.auth import authenticate @@ -187,7 +187,8 @@ class UserHostingKeyForm(forms.ModelForm): alerts the user of it. :return: """ - if 'generate' in self.request.POST: + if ('generate' in self.request.POST + or not self.fields['public_key'].required): return self.data.get('public_key') KEY_ERROR_MESSAGE = _("Please input a proper SSH key") openssh_pubkey_str = self.data.get('public_key').strip() @@ -214,10 +215,14 @@ class UserHostingKeyForm(forms.ModelForm): return openssh_pubkey_str def clean_name(self): + INVALID_NAME_MESSAGE = _("Comma not accepted in the name of the key") + if "," in self.data.get('name'): + logger.debug(INVALID_NAME_MESSAGE) + raise forms.ValidationError(INVALID_NAME_MESSAGE) return self.data.get('name') def clean_user(self): - return self.request.user + return self.request.user if self.request.user.is_authenticated() else None def clean(self): cleaned_data = self.cleaned_data diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 0e337cfb..14d48da9 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: 2018-09-08 08:45+0000\n" +"POT-Creation-Date: 2019-07-09 15:21+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -27,6 +27,30 @@ msgstr "Dein Account wurde noch nicht aktiviert." msgid "User does not exist" msgstr "Der Benutzer existiert nicht" +msgid "Choose a product" +msgstr "" + +msgid "Amount in CHF" +msgstr "Betrag" + +msgid "Recurring monthly" +msgstr "" + +msgid "Amount field does not match" +msgstr "" + +msgid "Recurring field does not match" +msgstr "" + +msgid "Product name" +msgstr "Produkt" + +msgid "Monthly subscription" +msgstr "" + +msgid "One time payment" +msgstr "" + msgid "Confirm Password" msgstr "Passwort Bestätigung" @@ -52,6 +76,9 @@ msgstr "Bitte verwende einen gültigen SSH-Key" msgid "This key exists already with the name \"%(name)s\"" msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits" +msgid "Comma not accepted in the name of the key" +msgstr "" + msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" @@ -209,7 +236,8 @@ msgstr "Du hast eine neue virtuelle Maschine bestellt!" #, python-format msgid "Your order of %(vm_name)s has been charged." -msgstr "Deine Bestellung von %(vm_name)s wurde entgegengenommen." +msgstr "" +"Deine Bestellung von %(vm_name)s wurde entgegengenommen." msgid "You can view your VM detail by clicking the button below." msgstr "Um die Rechnung zu sehen, klicke auf den Button unten." @@ -305,6 +333,100 @@ msgstr "Dashboard" msgid "Logout" msgstr "Abmelden" +#, python-format +msgid "%(page_header_text)s" +msgstr "" + +msgid "Invoice #" +msgstr "Rechnung" + +msgid "Date" +msgstr "Datum" + +msgid "Status" +msgstr "" + +msgid "Terminated" +msgstr "Beendet" + +msgid "Approved" +msgstr "Akzeptiert" + +msgid "Declined" +msgstr "Abgelehnt" + +msgid "Billed to" +msgstr "Rechnungsadresse" + +msgid "Payment method" +msgstr "Bezahlmethode" + +msgid "ending in" +msgstr "endend in" + +msgid "Invoice summary" +msgstr "" + +msgid "Product" +msgstr "Produkt" + +msgid "Period" +msgstr "Periode" + +msgid "Cores" +msgstr "Prozessorkerne" + +msgid "Memory" +msgstr "Arbeitsspeicher" + +msgid "Disk space" +msgstr "Festplattenkapazität" + +msgid "Subtotal" +msgstr "Zwischensumme" + +msgid "VAT" +msgstr "Mehrwertsteuer" + +msgid "Discount" +msgstr "Rabatt" + +msgid "Total" +msgstr "Gesamt" + +msgid "Amount" +msgstr "Betrag" + +msgid "Description" +msgstr "" + +msgid "Recurring" +msgstr "" + +msgid "of every month" +msgstr "" + +msgid "BACK TO LIST" +msgstr "ZURÜCK ZUR LISTE" + +msgid "Some problem encountered. Please try again later." +msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." + +msgid "VM ID" +msgstr "" + +msgid "IP Address" +msgstr "" + +msgid "See Invoice" +msgstr "Siehe Rechnung" + +msgid "Page" +msgstr "" + +msgid "of" +msgstr "" + msgid "Log in" msgstr "Anmelden" @@ -338,67 +460,15 @@ msgstr "Als gelesen markieren" msgid "All notifications" msgstr "Alle Benachrichtigungen" -#, python-format -msgid "%(page_header_text)s" -msgstr "" - -msgid "Date" -msgstr "Datum" - -msgid "Status" -msgstr "" - -msgid "Terminated" -msgstr "Beendet" - -msgid "Approved" -msgstr "Akzeptiert" - -msgid "Declined" -msgstr "Abgelehnt" - -msgid "Billed to" -msgstr "Rechnungsadresse" - -msgid "Payment method" -msgstr "Bezahlmethode" - -msgid "ending in" -msgstr "endend in" - msgid "Credit Card" msgstr "Kreditkarte" +msgid "Expiry" +msgstr "Gültig bis" + msgid "Order summary" msgstr "Bestellungsübersicht" -msgid "Product" -msgstr "Produkt" - -msgid "Period" -msgstr "Periode" - -msgid "Cores" -msgstr "Prozessorkerne" - -msgid "Memory" -msgstr "Arbeitsspeicher" - -msgid "Disk space" -msgstr "Festplattenkapazität" - -msgid "Subtotal" -msgstr "Zwischensumme" - -msgid "VAT" -msgstr "Mehrwertsteuer" - -msgid "Discount" -msgstr "Rabatt" - -msgid "Total" -msgstr "Gesamt" - #, python-format msgid "" "By clicking \"Place order\" this plan will charge your credit card account " @@ -410,9 +480,6 @@ msgstr "" msgid "Place order" msgstr "Bestellen" -msgid "BACK TO LIST" -msgstr "ZURÜCK ZUR LISTE" - msgid "Processing..." msgstr "Abarbeitung..." @@ -425,24 +492,9 @@ msgstr "" msgid "Close" msgstr "Schliessen" -msgid "Some problem encountered. Please try again later." -msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." - msgid "Order Nr." msgstr "Bestellung Nr." -msgid "Amount" -msgstr "Betrag" - -msgid "See Invoice" -msgstr "Siehe Rechnung" - -msgid "Page" -msgstr "" - -msgid "of" -msgstr "" - msgid "Your Order" msgstr "Deine Bestellung" @@ -539,9 +591,6 @@ msgstr "" "Wir nutzen Stripe für " "die Bezahlung und speichern keine Informationen in unserer Datenbank." -msgid "Add your public SSH key" -msgstr "Füge deinen öffentlichen SSH-Key hinzu" - msgid "Use your created key to access to the VM" msgstr "Benutze deinen erstellten SSH-Key um auf deine VM zugreifen zu können" @@ -783,6 +832,9 @@ msgstr "" msgid "Invalid number of cores" msgstr "Ungültige Anzahle CPU-Kerne" +msgid "Invalid calculator properties" +msgstr "" + msgid "Invalid RAM size" msgstr "Ungültige RAM-Grösse" @@ -821,6 +873,9 @@ msgstr "" "Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es " "noch einmal." +#~ msgid "Add your public SSH key" +#~ msgstr "Füge deinen öffentlichen SSH-Key hinzu" + #~ msgid "Do you want to cancel your Virtual Machine" #~ msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst" @@ -830,9 +885,6 @@ msgstr "" #~ msgid "My VM page" #~ msgstr "Meine VM page" -#~ msgid "Invoice Date" -#~ msgstr "Rechnung Datum" - #~ msgid "VM %(VM_ID)s terminated successfully" #~ msgstr "VM %(VM_ID)s erfolgreich beendet" diff --git a/hosting/migrations/0055_auto_20190701_1614.py b/hosting/migrations/0055_auto_20190701_1614.py new file mode 100644 index 00000000..4a2744fb --- /dev/null +++ b/hosting/migrations/0055_auto_20190701_1614.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2019-07-01 16:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0054_auto_20190508_2141'), + ] + + operations = [ + migrations.AlterField( + model_name='userhostingkey', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index c7a3dcde..bb7c5cc1 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -187,7 +187,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): class UserHostingKey(models.Model): - user = models.ForeignKey(CustomUser) + user = models.ForeignKey(CustomUser, blank=True, null=True) public_key = models.TextField() private_key = models.FileField(upload_to='private_keys', blank=True) created_at = models.DateTimeField(auto_now_add=True) @@ -613,6 +613,8 @@ class UserCardDetail(AssignPermissionsMixin, models.Model): for card in user_card_details: cards_list.append({ 'last4': card.last4, 'brand': card.brand, 'id': card.id, + 'exp_year': card.exp_year, + 'exp_month': '{:02d}'.format(card.exp_month), 'preferred': card.preferred }) return cards_list diff --git a/hosting/static/hosting/js/virtual_machine_detail.js b/hosting/static/hosting/js/virtual_machine_detail.js index 8f90933b..28592883 100644 --- a/hosting/static/hosting/js/virtual_machine_detail.js +++ b/hosting/static/hosting/js/virtual_machine_detail.js @@ -109,8 +109,11 @@ $(document).ready(function() { modal_btn = $('#createvm-modal-done-btn'); $('#createvm-modal-title').text(data.msg_title); $('#createvm-modal-body').html(data.msg_body); - modal_btn.attr('href', data.redirect) - .removeClass('hide'); + if (data.redirect) { + modal_btn.attr('href', data.redirect).removeClass('hide'); + } else { + modal_btn.attr('href', ""); + } if (data.status === true) { fa_icon.attr('class', 'checkmark'); } else { diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 4a62e9fa..a84e4e4f 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -82,6 +82,7 @@ {{user.email}} {% else %} {{cc_brand|default:_('Credit Card')}} {% trans "ending in" %} ****{{cc_last4}}
+ {% trans "Expiry" %} {{cc_exp_year}}/{{cc_exp_month}}
{% if request.user.is_authenticated %} {{request.user.email}} {% else %} diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index e09775cf..f0512fdb 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -131,6 +131,7 @@
{% trans "Credit Card" %}
{% trans "Last" %} 4: ***** {{card.last4}}
{% trans "Type" %}: {{card.brand}}
+
{% trans "Expiry" %}: {{card.exp_month}}/{{card.exp_year}}
{% trans "SELECT" %} diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html index 56818cbf..5cdd830c 100644 --- a/hosting/templates/hosting/settings.html +++ b/hosting/templates/hosting/settings.html @@ -37,6 +37,7 @@
{% trans "Credit Card" %}
{% trans "Last" %} 4: ***** {{card.last4}}
{% trans "Type" %}: {{card.brand}}
+
{% trans "Expiry" %}: {{card.exp_month}}/{{card.exp_year}}
{% if card_list_len > 1 %} diff --git a/hosting/templates/hosting/user_key.html b/hosting/templates/hosting/user_key.html index 804d661a..247551b5 100644 --- a/hosting/templates/hosting/user_key.html +++ b/hosting/templates/hosting/user_key.html @@ -8,7 +8,8 @@
{% csrf_token %} {% if messages %}
diff --git a/hosting/urls.py b/hosting/urls.py index 2c8ff8ab..5b2b87b0 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -1,5 +1,7 @@ from django.conf.urls import url from django.contrib.auth import views as auth_views + +from utils.views import SSHKeyCreateView, AskSSHKeyView from .views import ( DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, @@ -7,12 +9,11 @@ from .views import ( VirtualMachinesPlanListView, VirtualMachineView, OrdersHostingDeleteView, MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView, HostingPricingView, CreateVirtualMachinesView, HostingBillListView, - HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, + HostingBillDetailView, SSHKeyDeleteView, SSHKeyListView, SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView, InvoiceListView, InvoiceDetailView, CheckUserVM ) - urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), @@ -27,6 +28,8 @@ urlpatterns = [ url(r'invoices/?$', InvoiceListView.as_view(), name='invoices'), url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(), name='order-confirmation'), + url(r'^add-ssh-key/?$', AskSSHKeyView.as_view(), + name='add_ssh_key'), url(r'orders/(?P\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), url(r'invoice/(?P[-\w]+)/?$', InvoiceDetailView.as_view(), diff --git a/hosting/views.py b/hosting/views.py index 8d6c26e6..e0f5ebd3 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -49,6 +49,7 @@ from utils.forms import ( BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, ResendActivationEmailForm ) +from utils.hosting_utils import get_all_public_keys from utils.hosting_utils import get_vm_price_with_vat, HostingUtils from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils @@ -466,7 +467,9 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView): pk = self.kwargs.get('pk') # Get user ssh key public_key = UserHostingKey.objects.get(pk=pk).public_key - manager.manage_public_key([{'value': public_key, 'state': False}]) + keys = UserHostingKey.objects.filter(user=self.request.user) + keys_to_save = [k.public_key for k in keys if k.public_key != public_key] + manager.save_key_in_opennebula_user('\n'.join(keys_to_save), update_type=0) return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs) @@ -515,74 +518,11 @@ class SSHKeyChoiceView(LoginRequiredMixin, View): email=owner.email, password=owner.password ) - public_key_str = public_key.decode() - manager.manage_public_key([{'value': public_key_str, 'state': True}]) + keys = get_all_public_keys(request.user) + manager.save_key_in_opennebula_user('\n'.join(keys)) return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar') -@method_decorator(decorators, name='dispatch') -class SSHKeyCreateView(LoginRequiredMixin, FormView): - form_class = UserHostingKeyForm - model = UserHostingKey - template_name = 'hosting/user_key.html' - login_url = reverse_lazy('hosting:login') - context_object_name = "virtual_machine" - success_url = reverse_lazy('hosting:ssh_keys') - - def get_form_kwargs(self): - kwargs = super(SSHKeyCreateView, self).get_form_kwargs() - kwargs.update({'request': self.request}) - return kwargs - - def form_valid(self, form): - form.save() - if settings.DCL_SSH_KEY_NAME_PREFIX in form.instance.name: - content = ContentFile(form.cleaned_data.get('private_key')) - filename = form.cleaned_data.get( - 'name') + '_' + str(uuid.uuid4())[:8] + '_private.pem' - form.instance.private_key.save(filename, content) - context = self.get_context_data() - - next_url = self.request.session.get( - 'next', - reverse('hosting:create_virtual_machine') - ) - - if 'next' in self.request.session: - context.update({ - 'next_url': next_url - }) - del (self.request.session['next']) - - if form.cleaned_data.get('private_key'): - context.update({ - 'private_key': form.cleaned_data.get('private_key'), - 'key_name': form.cleaned_data.get('name'), - 'form': UserHostingKeyForm(request=self.request), - }) - - owner = self.request.user - manager = OpenNebulaManager( - email=owner.email, - password=owner.password - ) - public_key = form.cleaned_data['public_key'] - if type(public_key) is bytes: - public_key = public_key.decode() - manager.manage_public_key([{'value': public_key, 'state': True}]) - return HttpResponseRedirect(self.success_url) - - def post(self, request, *args, **kwargs): - form = self.get_form() - required = 'add_ssh' in self.request.POST - form.fields['name'].required = required - form.fields['public_key'].required = required - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - - @method_decorator(decorators, name='dispatch') class SettingsView(LoginRequiredMixin, FormView): template_name = "hosting/settings.html" @@ -829,21 +769,27 @@ class PaymentVMView(LoginRequiredMixin, FormView): reverse('hosting:payment') + '#payment_error') request.session['token'] = token request.session['billing_address_data'] = billing_address_data - return HttpResponseRedirect("{url}?{query_params}".format( - url=reverse('hosting:order-confirmation'), - query_params='page=payment') - ) + self.request.session['order_confirm_url'] = "{url}?{query_params}".format( + url=reverse('hosting:order-confirmation'), + query_params='page=payment') + return HttpResponseRedirect(reverse('hosting:add_ssh_key')) else: return self.form_invalid(form) -class OrdersHostingDetailView(LoginRequiredMixin, DetailView): +class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): + form_class = UserHostingKeyForm template_name = "hosting/order_detail.html" context_object_name = "order" login_url = reverse_lazy('hosting:login') permission_required = ['view_hostingorder'] model = HostingOrder + def get_form_kwargs(self): + kwargs = super(OrdersHostingDetailView, self).get_form_kwargs() + kwargs.update({'request': self.request}) + return kwargs + def get_object(self, queryset=None): order_id = self.kwargs.get('pk') try: @@ -868,6 +814,8 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): if self.request.GET.get('page') == 'payment': context['page_header_text'] = _('Confirm Order') + context['form'] = UserHostingKeyForm(request=self.request) + context['keys'] = get_all_public_keys(self.request.user) else: context['page_header_text'] = _('Invoice') if not self.request.user.has_perm( @@ -958,11 +906,15 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): card_details_response = card_details['response_object'] context['cc_last4'] = card_details_response['last4'] context['cc_brand'] = card_details_response['brand'] + context['cc_exp_year'] = card_details_response['exp_year'] + context['cc_exp_month'] = card_details_response['exp_month'] else: card_id = self.request.session.get('card_id') card_detail = UserCardDetail.objects.get(id=card_id) context['cc_last4'] = card_detail.last4 context['cc_brand'] = card_detail.brand + context['cc_exp_year'] = card_detail.exp_year + context['cc_exp_month'] = '{:02d}'.format(card_detail.exp_month) context['site_url'] = reverse('hosting:create_virtual_machine') context['vm'] = self.request.session.get('specs') return context @@ -1587,6 +1539,7 @@ class VirtualMachineView(LoginRequiredMixin, View): 'order': HostingOrder.objects.get( vm_id=serializer.data['vm_id'] ), + 'keys': UserHostingKey.objects.filter(user=request.user), 'has_invoices': False } try: @@ -1665,7 +1618,7 @@ class VirtualMachineView(LoginRequiredMixin, View): "manager.delete_vm returned False. Hence, error making " "xml-rpc call to delete vm failed." ) - response['text'] = ugettext('Error terminating VM') + vm.id + response['text'] = str(_('Error terminating VM')) + str(vm.id) else: for t in range(15): try: diff --git a/opennebula_api/models.py b/opennebula_api/models.py index a951349e..f8ef6481 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -207,22 +207,8 @@ class OpenNebulaManager(): else: vm_pool.info() return vm_pool - except AttributeError: - logger.error( - 'Could not connect via client, using oneadmin instead') - try: - vm_pool = oca.VirtualMachinePool(self.oneadmin_client) - if infoextended: - vm_pool.infoextended( - filter=-1, # User's resources and any of his groups - vm_state=-1 # Look for VMs in any state, except DONE - ) - else: - vm_pool.info(filter=-2) - return vm_pool - except: - raise ConnectionRefusedError - + except AttributeError as ae: + logger.error("AttributeError : %s" % str(ae)) except ConnectionRefusedError: logger.error( 'Could not connect to host: {host} via protocol {protocol}'.format( @@ -377,6 +363,31 @@ class OpenNebulaManager(): return vm_terminated + def save_key_in_opennebula_user(self, ssh_key, update_type=1): + """ + Save the given ssh key in OpenNebula user + + # Update type: 0: Replace the whole template. + 1: Merge new template with the existing one. + :param ssh_key: The ssh key to be saved + :param update_type: The update type as explained above + + :return: + """ + return_value = self.oneadmin_client.call( + 'user.update', + self.opennebula_user if type(self.opennebula_user) == int else self.opennebula_user.id, + '%s' % ssh_key, + update_type + ) + if type(return_value) == int: + logger.debug( + "Saved the key in opennebula successfully : %s" % return_value) + else: + logger.error( + "Could not save the key in opennebula. %s" % return_value) + return + def _get_template_pool(self): try: template_pool = oca.VmTemplatePool(self.oneadmin_client) diff --git a/utils/views.py b/utils/views.py index 394a9fc2..05d0fdc2 100644 --- a/utils/views.py +++ b/utils/views.py @@ -1,16 +1,25 @@ +import uuid + from django.conf import settings from django.contrib import messages from django.contrib.auth import authenticate, login from django.contrib.auth.tokens import default_token_generator +from django.core.files.base import ContentFile from django.core.urlresolvers import reverse_lazy from django.http import HttpResponseRedirect +from django.shortcuts import render from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView, CreateView from django.views.decorators.cache import cache_control +from django.views.generic import FormView, CreateView +from datacenterlight.utils import get_cms_integration +from hosting.forms import UserHostingKeyForm +from hosting.models import UserHostingKey from membership.models import CustomUser +from opennebula_api.models import OpenNebulaManager +from utils.hosting_utils import get_all_public_keys from .forms import SetPasswordForm from .mailer import BaseEmail @@ -174,3 +183,87 @@ class PasswordResetConfirmViewMixin(FormView): form.add_error(None, _('The reset password link is no longer valid.')) return self.form_invalid(form) + + +class SSHKeyCreateView(FormView): + form_class = UserHostingKeyForm + model = UserHostingKey + template_name = 'hosting/user_key.html' + login_url = reverse_lazy('hosting:login') + context_object_name = "virtual_machine" + success_url = reverse_lazy('hosting:ssh_keys') + + def get_form_kwargs(self): + kwargs = super(SSHKeyCreateView, self).get_form_kwargs() + kwargs.update({'request': self.request}) + return kwargs + + def form_valid(self, form): + form.save() + if settings.DCL_SSH_KEY_NAME_PREFIX in form.instance.name: + content = ContentFile(form.cleaned_data.get('private_key')) + filename = form.cleaned_data.get( + 'name') + '_' + str(uuid.uuid4())[:8] + '_private.pem' + form.instance.private_key.save(filename, content) + context = self.get_context_data() + + next_url = self.request.session.get( + 'next', + reverse_lazy('hosting:create_virtual_machine') + ) + + if 'next' in self.request.session: + context.update({ + 'next_url': next_url + }) + del (self.request.session['next']) + + if form.cleaned_data.get('private_key'): + context.update({ + 'private_key': form.cleaned_data.get('private_key'), + 'key_name': form.cleaned_data.get('name'), + 'form': UserHostingKeyForm(request=self.request), + }) + + if self.request.user.is_authenticated(): + owner = self.request.user + manager = OpenNebulaManager( + email=owner.email, + password=owner.password + ) + keys_to_save = get_all_public_keys(self.request.user) + manager.save_key_in_opennebula_user('\n'.join(keys_to_save)) + else: + self.request.session["new_user_hosting_key_id"] = form.instance.id + return HttpResponseRedirect(self.success_url) + + def post(self, request, *args, **kwargs): + form = self.get_form() + required = 'add_ssh' in self.request.POST + form.fields['name'].required = required + form.fields['public_key'].required = required + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + +class AskSSHKeyView(SSHKeyCreateView): + form_class = UserHostingKeyForm + template_name = "datacenterlight/add_ssh_key.html" + success_url = reverse_lazy('datacenterlight:order_confirmation') + context_object_name = "dcl_vm_buy_add_ssh_key" + + @cache_control(no_cache=True, must_revalidate=True, no_store=True) + def get(self, request, *args, **kwargs): + context = { + 'site_url': reverse_lazy('datacenterlight:index'), + 'cms_integration': get_cms_integration('default'), + 'form': UserHostingKeyForm(request=self.request), + 'keys': get_all_public_keys(self.request.user) + } + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + self.success_url = self.request.session.get("order_confirm_url") + return super(AskSSHKeyView, self).post(self, request, *args, **kwargs) \ No newline at end of file