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;
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.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:

View file

@ -134,6 +134,38 @@
</div>
<form id="virtual_machine_create_form" action="" method="POST">
{% 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="col-sm-8">
{% 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 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': "<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')
stripe_api_cus_id = request.session.get('customer')
stripe_utils = StripeUtils()

View file

@ -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):

View file

@ -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 {

View file

@ -198,6 +198,35 @@
{% block submit_btn %}
<form method="post" id="virtual_machine_create_form">
{% 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="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>

View file

@ -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'),

View file

@ -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': "<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')
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:

View file

@ -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,
'<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):
try:
template_pool = oca.VmTemplatePool(self.oneadmin_client)