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/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 31933e12..e1cc5853 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -134,6 +134,38 @@
{% csrf_token %} + {% if generic_payment_details %} + {% else %} + {% comment %} + We are in VM buy flow and we want user to click the "Place order" button. + At this point, we also want the user to input the SSH key for the VM. + {% endcomment %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} +
+

 {% trans "Add your public SSH key" %}

+
+
+ {% if keys|length > 0 %} +
Existing keys
+ {% endif %} + {% for key in keys %} + +
+ {% endfor %} +
+ {% for field in form %} + {% bootstrap_field field %} + {% endfor %} + {% endif %}
{% if generic_payment_details %} diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 5dc3a3d3..76f50aec 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -13,7 +13,8 @@ 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 @@ -24,7 +25,7 @@ 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 @@ -529,12 +530,18 @@ class PaymentOrderView(FormView): 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 = {} @@ -567,6 +574,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'), @@ -579,6 +588,31 @@ class OrderConfirmationView(DetailView): return render(request, self.template_name, context) def post(self, request, *args, **kwargs): + # Check ssh public key and then proceed + form = self.get_form() + required = True + + # SSH key validation is required only if the user doesn't have an + # existing key and user has input some value in the add ssh key fields + if (len(get_all_public_keys(self.request.user)) > 0 and + (len(form.data.get('public_key')) == 0 and + len(form.data.get('name')) == 0)): + required = False + form.fields['name'].required = required + form.fields['public_key'].required = required + if not form.is_valid(): + response = { + 'status': False, + 'msg_title': str(_('SSH key related error occurred')), + 'msg_body': "
".join([str(v) for k,v in form.errors.items()]), + } + return JsonResponse(response) + + if required: + # We have a valid SSH key from the user, save it in opennebula and + # db and proceed further + form.save() + user = request.session.get('user') stripe_api_cus_id = request.session.get('customer') stripe_utils = StripeUtils() diff --git a/hosting/forms.py b/hosting/forms.py index 16b06fe0..797bc700 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,6 +215,10 @@ 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): 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..7ab79378 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -198,6 +198,35 @@ {% block submit_btn %} {% csrf_token %} + {% comment %} + We are in VM buy flow and we want user to click the "Place order" button. + At this point, we also want the user to input the SSH key for the VM. + {% endcomment %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} +
+

 {% trans "Add your public SSH key" %}

+
+
+ {% if keys|length > 0 %} +
Existing keys
+ {% endif %} + {% for key in keys %} + +
+ {% endfor %} +
+ {% for field in form %} + {% bootstrap_field field %} + {% endfor %}
{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.
diff --git a/hosting/urls.py b/hosting/urls.py index 2c8ff8ab..4779f67c 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -1,5 +1,6 @@ from django.conf.urls import url from django.contrib.auth import views as auth_views + from .views import ( DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, @@ -12,7 +13,6 @@ from .views import ( InvoiceListView, InvoiceDetailView, CheckUserVM ) - urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), diff --git a/hosting/views.py b/hosting/views.py index 4c6ea04d..f5146fbf 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,8 +518,8 @@ 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') @@ -566,10 +569,8 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView): 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}]) + keys_to_save = get_all_public_keys(self.request.user) + manager.save_key_in_opennebula_user('\n'.join(keys_to_save)) return HttpResponseRedirect(self.success_url) def post(self, request, *args, **kwargs): @@ -837,13 +838,19 @@ class PaymentVMView(LoginRequiredMixin, FormView): 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 +875,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( @@ -993,6 +1002,31 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): @method_decorator(decorators) def post(self, request): + # Check ssh public key and then proceed + form = self.get_form() + required = True + + # SSH key validation is required only if the user doesn't have an + # existing key and user has input some value in the add ssh key fields + if (len(get_all_public_keys(self.request.user)) > 0 and + (len(form.data.get('public_key')) == 0 and + len(form.data.get('name')) == 0)): + required = False + form.fields['name'].required = required + form.fields['public_key'].required = required + if not form.is_valid(): + response = { + 'status': False, + 'msg_title': str(_('SSH key related error occurred')), + 'msg_body': "
".join([str(v) for k,v in form.errors.items()]), + } + return JsonResponse(response) + + if required: + # We have a valid SSH key from the user, save it in opennebula and + # db and proceed further + form.save() + template = request.session.get('template') specs = request.session.get('specs') stripe_utils = StripeUtils() @@ -1573,7 +1607,8 @@ class VirtualMachineView(LoginRequiredMixin, View): 'virtual_machine': serializer.data, 'order': HostingOrder.objects.get( vm_id=serializer.data['vm_id'] - ) + ), + 'keys': UserHostingKey.objects.filter(user=request.user) } except Exception as ex: logger.debug("Exception generated {}".format(str(ex))) @@ -1639,7 +1674,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..478758fc 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.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)