Merge branch 'task/5509/add-keys-to-opennebula-user' into 'master'

Save user's key in opennebula

See merge request ungleich-public/dynamicweb!704
This commit is contained in:
pcoder116 2019-06-25 14:00:44 +02:00
commit 59a78dd8bb
10 changed files with 195 additions and 94 deletions

View file

@ -186,3 +186,8 @@ footer .dcl-link-separator::before {
background: transparent !important; background: transparent !important;
resize: none; resize: none;
} }
.existing-keys-title {
font-weight: bold;
font-size: 14px;
}

View file

@ -8,7 +8,6 @@ from django.core.mail import EmailMessage
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from time import sleep
from dynamicweb.celery import app from dynamicweb.celery import app
from hosting.models import HostingOrder from hosting.models import HostingOrder
@ -16,7 +15,7 @@ from membership.models import CustomUser
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import VirtualMachineSerializer from opennebula_api.serializers import VirtualMachineSerializer
from utils.hosting_utils import ( 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.mailer import BaseEmail
from utils.stripe_utils import StripeUtils 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 # Create OpenNebulaManager
manager = OpenNebulaManager(email=on_user, password=on_pass) 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( vm_id = manager.create_vm(
template_id=vm_template_id, template_id=vm_template_id,
specs=specs, specs=specs,
ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY, ssh_key='\n'.join(pub_keys),
vm_name=vm_name 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 = BaseEmail(**email_data)
email.send() 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)) logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if vm_ipv6 is not None: if vm_id > 0:
custom_user = CustomUser.objects.get(email=user.get('email'))
get_or_create_vm_detail(custom_user, manager, vm_id) 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: except Exception as e:
logger.error(str(e)) logger.error(str(e))
try: try:

View file

@ -134,6 +134,38 @@
</div> </div>
<form id="virtual_machine_create_form" action="" method="POST"> <form id="virtual_machine_create_form" action="" method="POST">
{% csrf_token %} {% 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 %}
<div class="alert alert-warning">
{% for message in messages %}
<span>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
<div class="dashboard-container-head">
<h2 class="dashboard-title-thin"><i class="fa fa-key" aria-hidden="true"></i>&nbsp;{% trans "Add your public SSH key" %}</h2>
</div>
<div class="existing-keys">
{% if keys|length > 0 %}
<div class="existing-keys-title">Existing keys</div>
{% endif %}
{% for key in keys %}
<textarea class="form-control input-no-border" style="width: 100%" readonly rows="6">
{{key}}
</textarea>
<br/>
{% endfor %}
</div>
{% for field in form %}
{% bootstrap_field field %}
{% endfor %}
{% endif %}
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
{% if generic_payment_details %} {% if generic_payment_details %}

View file

@ -13,7 +13,8 @@ from django.views.decorators.cache import cache_control
from django.views.generic import FormView, CreateView, DetailView from django.views.generic import FormView, CreateView, DetailView
from hosting.forms import ( from hosting.forms import (
HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm,
UserHostingKeyForm
) )
from hosting.models import ( from hosting.models import (
HostingBill, HostingOrder, UserCardDetail, GenericProduct HostingBill, HostingOrder, UserCardDetail, GenericProduct
@ -24,7 +25,7 @@ from utils.forms import (
BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm, BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm,
BillingAddress 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.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task from utils.tasks import send_plain_email_task
from .cms_models import DCLCalculatorPluginModel from .cms_models import DCLCalculatorPluginModel
@ -529,12 +530,18 @@ class PaymentOrderView(FormView):
return self.render_to_response(context) return self.render_to_response(context)
class OrderConfirmationView(DetailView): class OrderConfirmationView(DetailView, FormView):
form_class = UserHostingKeyForm
template_name = "datacenterlight/order_detail.html" template_name = "datacenterlight/order_detail.html"
payment_template_name = 'datacenterlight/landing_payment.html' payment_template_name = 'datacenterlight/landing_payment.html'
context_object_name = "order" context_object_name = "order"
model = HostingOrder 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) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
context = {} context = {}
@ -567,6 +574,8 @@ class OrderConfirmationView(DetailView):
else: else:
context.update({ context.update({
'vm': request.session.get('specs'), 'vm': request.session.get('specs'),
'form': UserHostingKeyForm(request=self.request),
'keys': get_all_public_keys(self.request.user)
}) })
context.update({ context.update({
'site_url': reverse('datacenterlight:index'), 'site_url': reverse('datacenterlight:index'),
@ -579,6 +588,31 @@ class OrderConfirmationView(DetailView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
def post(self, request, *args, **kwargs): 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': "<br/>".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') user = request.session.get('user')
stripe_api_cus_id = request.session.get('customer') stripe_api_cus_id = request.session.get('customer')
stripe_utils = StripeUtils() stripe_utils = StripeUtils()

View file

@ -1,8 +1,8 @@
import datetime import datetime
import logging import logging
import subprocess import subprocess
import tempfile import tempfile
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
@ -187,7 +187,8 @@ class UserHostingKeyForm(forms.ModelForm):
alerts the user of it. alerts the user of it.
:return: :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') return self.data.get('public_key')
KEY_ERROR_MESSAGE = _("Please input a proper SSH key") KEY_ERROR_MESSAGE = _("Please input a proper SSH key")
openssh_pubkey_str = self.data.get('public_key').strip() openssh_pubkey_str = self.data.get('public_key').strip()
@ -214,6 +215,10 @@ class UserHostingKeyForm(forms.ModelForm):
return openssh_pubkey_str return openssh_pubkey_str
def clean_name(self): 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') return self.data.get('name')
def clean_user(self): def clean_user(self):

View file

@ -109,8 +109,11 @@ $(document).ready(function() {
modal_btn = $('#createvm-modal-done-btn'); modal_btn = $('#createvm-modal-done-btn');
$('#createvm-modal-title').text(data.msg_title); $('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').html(data.msg_body); $('#createvm-modal-body').html(data.msg_body);
modal_btn.attr('href', data.redirect) if (data.redirect) {
.removeClass('hide'); modal_btn.attr('href', data.redirect).removeClass('hide');
} else {
modal_btn.attr('href', "");
}
if (data.status === true) { if (data.status === true) {
fa_icon.attr('class', 'checkmark'); fa_icon.attr('class', 'checkmark');
} else { } else {

View file

@ -198,6 +198,35 @@
{% block submit_btn %} {% block submit_btn %}
<form method="post" id="virtual_machine_create_form"> <form method="post" id="virtual_machine_create_form">
{% csrf_token %} {% 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 %}
<div class="alert alert-warning">
{% for message in messages %}
<span>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
<div class="dashboard-container-head">
<h2 class="dashboard-title-thin"><i class="fa fa-key" aria-hidden="true"></i>&nbsp;{% trans "Add your public SSH key" %}</h2>
</div>
<div class="existing-keys">
{% if keys|length > 0 %}
<div class="existing-keys-title">Existing keys</div>
{% endif %}
{% for key in keys %}
<textarea class="form-control input-no-border" style="width: 100%" readonly rows="6">
{{key}}
</textarea>
<br/>
{% endfor %}
</div>
{% for field in form %}
{% bootstrap_field field %}
{% endfor %}
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
<div class="dcl-place-order-text">{% 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 %}.</div> <div class="dcl-place-order-text">{% 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 %}.</div>

View file

@ -1,5 +1,6 @@
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from .views import ( from .views import (
DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView, DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView,
LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView,
@ -12,7 +13,6 @@ from .views import (
InvoiceListView, InvoiceDetailView, CheckUserVM InvoiceListView, InvoiceDetailView, CheckUserVM
) )
urlpatterns = [ urlpatterns = [
url(r'index/?$', IndexView.as_view(), name='index'), url(r'index/?$', IndexView.as_view(), name='index'),
url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'),

View file

@ -49,6 +49,7 @@ from utils.forms import (
BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm,
ResendActivationEmailForm ResendActivationEmailForm
) )
from utils.hosting_utils import get_all_public_keys
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
from utils.mailer import BaseEmail from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
@ -466,7 +467,9 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
# Get user ssh key # Get user ssh key
public_key = UserHostingKey.objects.get(pk=pk).public_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) return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs)
@ -515,8 +518,8 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
email=owner.email, email=owner.email,
password=owner.password password=owner.password
) )
public_key_str = public_key.decode() keys = get_all_public_keys(request.user)
manager.manage_public_key([{'value': public_key_str, 'state': True}]) manager.save_key_in_opennebula_user('\n'.join(keys))
return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar') return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar')
@ -566,10 +569,8 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView):
email=owner.email, email=owner.email,
password=owner.password password=owner.password
) )
public_key = form.cleaned_data['public_key'] keys_to_save = get_all_public_keys(self.request.user)
if type(public_key) is bytes: manager.save_key_in_opennebula_user('\n'.join(keys_to_save))
public_key = public_key.decode()
manager.manage_public_key([{'value': public_key, 'state': True}])
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -837,13 +838,19 @@ class PaymentVMView(LoginRequiredMixin, FormView):
return self.form_invalid(form) return self.form_invalid(form)
class OrdersHostingDetailView(LoginRequiredMixin, DetailView): class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
form_class = UserHostingKeyForm
template_name = "hosting/order_detail.html" template_name = "hosting/order_detail.html"
context_object_name = "order" context_object_name = "order"
login_url = reverse_lazy('hosting:login') login_url = reverse_lazy('hosting:login')
permission_required = ['view_hostingorder'] permission_required = ['view_hostingorder']
model = 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): def get_object(self, queryset=None):
order_id = self.kwargs.get('pk') order_id = self.kwargs.get('pk')
try: try:
@ -868,6 +875,8 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
if self.request.GET.get('page') == 'payment': if self.request.GET.get('page') == 'payment':
context['page_header_text'] = _('Confirm Order') context['page_header_text'] = _('Confirm Order')
context['form'] = UserHostingKeyForm(request=self.request)
context['keys'] = get_all_public_keys(self.request.user)
else: else:
context['page_header_text'] = _('Invoice') context['page_header_text'] = _('Invoice')
if not self.request.user.has_perm( if not self.request.user.has_perm(
@ -993,6 +1002,31 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
@method_decorator(decorators) @method_decorator(decorators)
def post(self, request): 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': "<br/>".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') template = request.session.get('template')
specs = request.session.get('specs') specs = request.session.get('specs')
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
@ -1573,7 +1607,8 @@ class VirtualMachineView(LoginRequiredMixin, View):
'virtual_machine': serializer.data, 'virtual_machine': serializer.data,
'order': HostingOrder.objects.get( 'order': HostingOrder.objects.get(
vm_id=serializer.data['vm_id'] vm_id=serializer.data['vm_id']
) ),
'keys': UserHostingKey.objects.filter(user=request.user)
} }
except Exception as ex: except Exception as ex:
logger.debug("Exception generated {}".format(str(ex))) logger.debug("Exception generated {}".format(str(ex)))
@ -1639,7 +1674,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
"manager.delete_vm returned False. Hence, error making " "manager.delete_vm returned False. Hence, error making "
"xml-rpc call to delete vm failed." "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: else:
for t in range(15): for t in range(15):
try: try:

View file

@ -207,22 +207,8 @@ class OpenNebulaManager():
else: else:
vm_pool.info() vm_pool.info()
return vm_pool return vm_pool
except AttributeError: except AttributeError as ae:
logger.error( logger.error("AttributeError : %s" % str(ae))
'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 ConnectionRefusedError: except ConnectionRefusedError:
logger.error( logger.error(
'Could not connect to host: {host} via protocol {protocol}'.format( 'Could not connect to host: {host} via protocol {protocol}'.format(
@ -377,6 +363,31 @@ class OpenNebulaManager():
return vm_terminated 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,
'<CONTEXT><SSH_PUBLIC_KEY>%s</SSH_PUBLIC_KEY></CONTEXT>' % 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): def _get_template_pool(self):
try: try:
template_pool = oca.VmTemplatePool(self.oneadmin_client) template_pool = oca.VmTemplatePool(self.oneadmin_client)