merged master

This commit is contained in:
Arvind Tiwari 2017-09-20 23:21:24 +05:30
commit fc03df4652
15 changed files with 836 additions and 284 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-11 23:47+0530\n" "POT-Creation-Date: 2017-09-16 14:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#, python-format
msgid "Your New VM %(vm_name)s at Data Center Light"
msgstr "Deine neue VM %(vm_name)s bei Data Center Light"
msgid "Enter name" msgid "Enter name"
msgstr "Name" msgstr "Name"

View file

@ -1,15 +1,22 @@
from dynamicweb.celery import app from datetime import datetime
from celery.exceptions import MaxRetriesExceededError
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from celery import current_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage
from django.utils import translation
from django.utils.translation import ugettext_lazy as _
from dynamicweb.celery import app
from hosting.models import HostingOrder, HostingBill
from membership.models import StripeCustomer, CustomUser
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import VirtualMachineSerializer from opennebula_api.serializers import VirtualMachineSerializer
from hosting.models import HostingOrder, HostingBill from utils.hosting_utils import get_all_public_keys
from utils.forms import UserBillingAddressForm from utils.forms import UserBillingAddressForm
from datetime import datetime from utils.mailer import BaseEmail
from membership.models import StripeCustomer
from django.core.mail import EmailMessage
from utils.models import BillingAddress from utils.models import BillingAddress
from celery.exceptions import MaxRetriesExceededError
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
@ -45,25 +52,36 @@ def create_vm_task(self, vm_template_id, user, specs, template,
stripe_customer_id, billing_address_data, stripe_customer_id, billing_address_data,
billing_address_id, billing_address_id,
charge, cc_details): charge, cc_details):
logger.debug("Running create_vm_task on {}".format(current_task.request.hostname))
vm_id = None vm_id = None
try: try:
final_price = specs.get('price') final_price = specs.get('price')
billing_address = BillingAddress.objects.filter( billing_address = BillingAddress.objects.filter(
id=billing_address_id).first() id=billing_address_id).first()
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
# Create OpenNebulaManager
manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME,
password=settings.OPENNEBULA_PASSWORD)
# Create a vm using oneadmin, also specify the name if 'pass' in user:
vm_id = manager.create_vm( on_user = user.get('email')
template_id=vm_template_id, on_pass = user.get('pass')
specs=specs, logger.debug("Using user {user} to create VM".format(user=on_user))
ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY, vm_name = None
else:
on_user = settings.OPENNEBULA_USERNAME
on_pass = settings.OPENNEBULA_PASSWORD
logger.debug("Using OpenNebula admin user to create VM")
vm_name = "{email}-{template_name}-{date}".format( vm_name = "{email}-{template_name}-{date}".format(
email=user.get('email'), email=user.get('email'),
template_name=template.get('name'), template_name=template.get('name'),
date=int(datetime.now().strftime("%s"))) date=int(datetime.now().strftime("%s")))
# Create OpenNebulaManager
manager = OpenNebulaManager(email=on_user, password=on_pass)
vm_id = manager.create_vm(
template_id=vm_template_id,
specs=specs,
ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY,
vm_name=vm_name
) )
if vm_id is None: if vm_id is None:
@ -122,6 +140,54 @@ def create_vm_task(self, vm_template_id, user, specs, template,
} }
email = EmailMessage(**email_data) email = EmailMessage(**email_data)
email.send() email.send()
if 'pass' in user:
lang = 'en-us'
if user.get('language') is not None:
logger.debug("Language is set to {}".format(user.get('language')))
lang = user.get('language')
translation.activate(lang)
# Send notification to the user as soon as VM has been booked
context = {
'vm': vm,
'order': order,
'base_url': "{0}://{1}".format(user.get('request_scheme'),
user.get('request_host')),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')}
}
email_data = {
'subject': context.get('page_header'),
'to': user.get('email'),
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
# try to see if we have the IP and that if the ssh keys can
# be configured
new_host = manager.get_primary_ipv4(vm_id)
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if new_host is not None:
custom_user = CustomUser.objects.get(email=user.get('email'))
if custom_user is not None:
public_keys = get_all_public_keys(custom_user)
keys = [{'value': key, 'state': True} for key in
public_keys]
if len(keys) > 0:
logger.debug(
"Calling configure on {host} for {num_keys} keys".format(
host=new_host, num_keys=len(keys)))
# Let's delay the task by 75 seconds to be sure
# that we run the cdist configure after the host
# is up
manager.manage_public_key(keys,
hosts=[new_host],
countdown=75)
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
try: try:
@ -134,8 +200,8 @@ def create_vm_task(self, vm_template_id, user, specs, template,
email_data = { email_data = {
'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT, 'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT,
msg_text), msg_text),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, 'from_email': current_task.request.hostname,
'to': ['info@ungleich.ch'], 'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': ',\n'.join(str(i) for i in self.request.args) 'body': ',\n'.join(str(i) for i in self.request.args)
} }
email = EmailMessage(**email_data) email = EmailMessage(**email_data)

View file

@ -115,8 +115,8 @@ class CeleryTaskTestCase(TestCase):
'response_object').stripe_plan_id}]) 'response_object').stripe_plan_id}])
stripe_subscription_obj = subscription_result.get('response_object') stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active # Check if the subscription was approved and is active
if stripe_subscription_obj is None or \ if stripe_subscription_obj is None \
stripe_subscription_obj.status != 'active': or stripe_subscription_obj.status != 'active':
msg = subscription_result.get('error') msg = subscription_result.get('error')
raise Exception("Creating subscription failed: {}".format(msg)) raise Exception("Creating subscription failed: {}".format(msg))

View file

@ -173,7 +173,8 @@ TEMPLATES = [
os.path.join(PROJECT_DIR, 'nosystemd/templates/'), os.path.join(PROJECT_DIR, 'nosystemd/templates/'),
os.path.join(PROJECT_DIR, os.path.join(PROJECT_DIR,
'ungleich/templates/djangocms_blog/'), 'ungleich/templates/djangocms_blog/'),
os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'), os.path.join(PROJECT_DIR,
'ungleich/templates/cms/ungleichch'),
os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'), os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'),
os.path.join(PROJECT_DIR, os.path.join(PROJECT_DIR,
'ungleich_page/templates/ungleich_page'), 'ungleich_page/templates/ungleich_page'),
@ -559,9 +560,21 @@ CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Zurich' # CELERY_TIMEZONE = 'Europe/Zurich'
CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5)
DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO')
DCL_ERROR_EMAILS_TO_LIST = []
if DCL_ERROR_EMAILS_TO is not None:
DCL_ERROR_EMAILS_TO_LIST = [x.strip() for x in
DCL_ERROR_EMAILS_TO.split(
',')] \
if "," in DCL_ERROR_EMAILS_TO else [DCL_ERROR_EMAILS_TO.strip()]
if 'info@ungleich.ch' not in DCL_ERROR_EMAILS_TO_LIST:
DCL_ERROR_EMAILS_TO_LIST.append('info@ungleich.ch')
ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING') ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING')
if ENABLE_DEBUG_LOGGING: if ENABLE_DEBUG_LOGGING:
@ -585,6 +598,9 @@ if ENABLE_DEBUG_LOGGING:
}, },
} }
TEST_MANAGE_SSH_KEY_PUBKEY = env('TEST_MANAGE_SSH_KEY_PUBKEY')
TEST_MANAGE_SSH_KEY_HOST = env('TEST_MANAGE_SSH_KEY_HOST')
DEBUG = bool_env('DEBUG') DEBUG = bool_env('DEBUG')
if DEBUG: if DEBUG:

View file

@ -1,16 +1,22 @@
import datetime import datetime
import logging
import subprocess
import tempfile
from django import forms from django import forms
from membership.models import CustomUser
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from membership.models import CustomUser
from utils.hosting_utils import get_all_public_keys
from .models import UserHostingKey from .models import UserHostingKey
logger = logging.getLogger(__name__)
def generate_ssh_key_name(): def generate_ssh_key_name():
return 'dcl-generated-key-' + datetime.datetime.now().strftime('%m%d%y%H%M') return 'dcl-generated-key-' + datetime.datetime.now().strftime(
'%m%d%y%H%M')
class HostingUserLoginForm(forms.Form): class HostingUserLoginForm(forms.Form):
@ -38,9 +44,7 @@ class HostingUserLoginForm(forms.Form):
CustomUser.objects.get(email=email) CustomUser.objects.get(email=email)
return email return email
except CustomUser.DoesNotExist: except CustomUser.DoesNotExist:
raise forms.ValidationError("User does not exist") raise forms.ValidationError(_("User does not exist"))
else:
return email
class HostingUserSignupForm(forms.ModelForm): class HostingUserSignupForm(forms.ModelForm):
@ -51,7 +55,8 @@ class HostingUserSignupForm(forms.ModelForm):
model = CustomUser model = CustomUser
fields = ['name', 'email', 'password'] fields = ['name', 'email', 'password']
widgets = { widgets = {
'name': forms.TextInput(attrs={'placeholder': 'Enter your name or company name'}), 'name': forms.TextInput(
attrs={'placeholder': 'Enter your name or company name'}),
} }
def clean_confirm_password(self): def clean_confirm_password(self):
@ -65,19 +70,55 @@ class HostingUserSignupForm(forms.ModelForm):
class UserHostingKeyForm(forms.ModelForm): class UserHostingKeyForm(forms.ModelForm):
private_key = forms.CharField(widget=forms.HiddenInput(), required=False) private_key = forms.CharField(widget=forms.HiddenInput(), required=False)
public_key = forms.CharField(widget=forms.Textarea( public_key = forms.CharField(widget=forms.Textarea(
attrs={'class': 'form_public_key', 'placeholder': _('Paste here your public key')}), attrs={'class': 'form_public_key',
'placeholder': _('Paste here your public key')}),
required=False, required=False,
) )
user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(), user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(),
required=False, widget=forms.HiddenInput()) required=False,
widget=forms.HiddenInput())
name = forms.CharField(required=False, widget=forms.TextInput( name = forms.CharField(required=False, widget=forms.TextInput(
attrs={'class': 'form_key_name', 'placeholder': _('Give a name to your key')})) attrs={'class': 'form_key_name',
'placeholder': _('Give a name to your key')}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request") self.request = kwargs.pop("request")
super(UserHostingKeyForm, self).__init__(*args, **kwargs) super(UserHostingKeyForm, self).__init__(*args, **kwargs)
self.fields['name'].label = _('Key name') self.fields['name'].label = _('Key name')
def clean_public_key(self):
"""
Validates a public ssh key using `ssh-keygen -lf key.pub`
Also checks if a given key already exists in the database and
alerts the user of it.
:return:
"""
if 'generate' in self.request.POST:
return self.data.get('public_key')
KEY_ERROR_MESSAGE = _("Please input a proper SSH key")
openssh_pubkey_str = self.data.get('public_key').strip()
if openssh_pubkey_str in get_all_public_keys(self.request.user):
key_name = UserHostingKey.objects.filter(
user_id=self.request.user.id,
public_key=openssh_pubkey_str).first().name
KEY_EXISTS_MESSAGE = _(
"This key exists already with the name \"%(name)s\"") % {
'name': key_name}
raise forms.ValidationError(KEY_EXISTS_MESSAGE)
with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:
tmp_public_key_file.write(openssh_pubkey_str.encode('utf-8'))
tmp_public_key_file.flush()
try:
subprocess.check_output(
['ssh-keygen', '-lf', tmp_public_key_file.name])
except subprocess.CalledProcessError as cpe:
logger.debug(
"Not a correct ssh format {error}".format(error=str(cpe)))
raise forms.ValidationError(KEY_ERROR_MESSAGE)
return openssh_pubkey_str
def clean_name(self): def clean_name(self):
return self.data.get('name') return self.data.get('name')

View file

@ -8,7 +8,11 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
<<<<<<< HEAD
"POT-Creation-Date: 2017-09-11 23:47+0530\n" "POT-Creation-Date: 2017-09-11 23:47+0530\n"
=======
"POT-Creation-Date: 2017-09-14 12:27+0000\n"
>>>>>>> master
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -24,6 +28,9 @@ msgstr "Dein Benutzername und/oder Dein Passwort ist falsch."
msgid "Your account is not activated yet." msgid "Your account is not activated yet."
msgstr "Dein Account wurde noch nicht aktiviert." msgstr "Dein Account wurde noch nicht aktiviert."
msgid "User does not exist"
msgstr "Der Benutzer existiert nicht"
msgid "Paste here your public key" msgid "Paste here your public key"
msgstr "Füge deinen Public Key ein" msgstr "Füge deinen Public Key ein"
@ -33,6 +40,13 @@ msgstr "Gebe deinem SSH-Key einen Name"
msgid "Key name" msgid "Key name"
msgstr "Key-Name" msgstr "Key-Name"
msgid "Please input a proper SSH key"
msgstr "Bitte verwende einen gültigen SSH-Key"
#, python-format
msgid "This key exists already with the name \"%(name)s\""
msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"
msgid "All Rights Reserved" msgid "All Rights Reserved"
msgstr "Alle Rechte vorbehalten" msgstr "Alle Rechte vorbehalten"

View file

@ -1,5 +1,35 @@
$(document).ready(function () { $(document).ready(function () {
var create_vm_form = $('#virtual_machine_create_form');
create_vm_form.submit(function () {
$('#btn-create-vm').prop('disabled', true);
$.ajax({
url: create_vm_form.attr('action'),
type: 'POST',
data: create_vm_form.serialize(),
success: function (data) {
if (data.status === true) {
fa_icon = $('.modal-icon > .fa');
fa_icon.attr('class', 'fa fa-check');
$('.modal-header > .close').attr('class', 'close');
$('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').text(data.msg_body);
$('#createvm-modal').on('hidden.bs.modal', function () {
window.location = data.redirect;
})
}
},
error: function (xmlhttprequest, textstatus, message) {
fa_icon = $('.modal-icon > .fa');
fa_icon.attr('class', 'fa fa-times');
$('.modal-header > .close').attr('class', 'close');
if (typeof(create_vm_error_message) !== 'undefined') {
$('#createvm-modal-title').text(create_vm_error_message);
}
$('#btn-create-vm').prop('disabled', false);
}
});
return false;
});
$('#confirm-cancel').on('click', '.btn-ok', function (e) { $('#confirm-cancel').on('click', '.btn-ok', function (e) {
$('#virtual_machine_cancel_form').trigger('submit'); $('#virtual_machine_cancel_form').trigger('submit');

View file

@ -1,6 +1,8 @@
{% extends "hosting/base_short.html" %} {% extends "hosting/base_short.html" %}
{% load staticfiles bootstrap3 %} {% load staticfiles bootstrap3 %}
{% load i18n %} {% load i18n %}
{% load custom_tags %}
{% block content %} {% block content %}
<div class="order-detail-container"> <div class="order-detail-container">
@ -20,30 +22,58 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-8 col-md-offset-2"> <div class="col-xs-12 col-md-8 col-md-offset-2">
<div class="invoice-title"> <div class="invoice-title">
<h2>{{page_header_text}}</h2><h3 class="pull-right">{% trans "Order #"%} {{order.id}}</h3> <h2>{{page_header_text}}</h2>
<h3 class="pull-right">
{% if order %}
{% trans "Order #"%} {{order.id}}
{% endif %}
</h3>
</div> </div>
<hr> <hr>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-6 pull-right order-confirm-date"> <div class="col-xs-12 col-md-6 pull-right order-confirm-date">
<address> <address>
<strong>{% trans "Date"%}:</strong><br> <strong>{% trans "Date"%}:</strong><br>
<span id="order-created_at">{{order.created_at|date:'Y-m-d H:i'}}</span><br><br> <span id="order-created_at">
{% if order %}
{{order.created_at|date:'Y-m-d H:i'}}
{% else %}
{% now "Y-m-d H:i" %}
{% endif %}
</span><br><br>
{% if order %}
<strong>{% trans "Status:"%}</strong><br> <strong>{% trans "Status:"%}</strong><br>
{% if order.status == 'Approved' %} {% if order.status == 'Approved' %}
<strong class="text-success">{% trans "Approved" %}</strong> <strong class="text-success">
{% trans "Approved" %}
</strong>
{% else %} {% else %}
<strong class="text-danger">{% trans "Declined" %}</strong> <strong class="text-danger">
{% trans "Declined" %}
</strong>
{% endif %} {% endif %}
<br><br> <br><br>
{% endif %}
</address> </address>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<address> <address>
<h3><b>{% trans "Billed To:"%}</b></h3> <h3><b>{% trans "Billed To:"%}</b></h3>
{% if order %}
{{user.name}}<br> {{user.name}}<br>
{{order.billing_address.street_address}},{{order.billing_address.postal_code}}<br> {{order.billing_address.street_address}},{{order.billing_address.postal_code}}<br>
{{order.billing_address.city}}, {{order.billing_address.country}}. {{order.billing_address.city}},
{{order.billing_address.country}}.
{% else %}
{% with request.session.billing_address_data as billing_address %}
{{billing_address|get_value_from_dict:'cardholder_name'}}<br>
{{billing_address|get_value_from_dict:'street_address'}},
{{billing_address|get_value_from_dict:'postal_code'}}<br>
{{billing_address|get_value_from_dict:'city'}},
{{billing_address|get_value_from_dict:'country'}}.
{% endwith %}
{% endif %}
</address> </address>
</div> </div>
@ -52,8 +82,15 @@
<div class="col-xs-6"> <div class="col-xs-6">
<address> <address>
<strong>{% trans "Payment Method:"%}</strong><br> <strong>{% trans "Payment Method:"%}</strong><br>
{{order.cc_brand}} {% trans "ending in" %} **** {{order.last4}}<br> {% if order %}
{{order.cc_brand}} {% trans "ending in" %} ****
{{order.last4}}<br>
{{user.email}} {{user.email}}
{% else %}
{{cc_brand}} {% trans "ending in" %} ****
{{cc_last4}}<br>
{{request.session.user.email}}
{% endif %}
</address> </address>
</div> </div>
</div> </div>
@ -65,28 +102,108 @@
<h3><b>{% trans "Order summary"%}</b></h3> <h3><b>{% trans "Order summary"%}</b></h3>
<hr> <hr>
<div class="content"> <div class="content">
<p><b>{% trans "Cores"%}</b> <span class="pull-right">{{vm.cores}}</span></p> {% if request.session.specs %}
{% with request.session.specs as vm %}
<p><b>{% trans "Cores"%}</b>
<span class="pull-right">{{vm.cpu}}</span>
</p>
<hr> <hr>
<p><b>{% trans "Memory"%}</b> <span class="pull-right">{{vm.memory}} GB</span></p> <p><b>{% trans "Memory"%}</b>
<span class="pull-right">{{vm.memory}} GB</span>
</p>
<hr> <hr>
<p><b>{% trans "Disk space"%}</b> <span class="pull-right">{{vm.disk_size}} GB</span></p> <p><b>{% trans "Disk space"%}</b>
<span class="pull-right">{{vm.disk_size}} GB</span>
</p>
<hr> <hr>
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b></p></h4> <p><b>{% trans "Configuration"%}</b>
<span class="pull-right">{{request.session.template.name}}</span>
</p>
<hr>
<h4>{% trans "Total"%}
<p class="pull-right">
<b>{{vm.price}} CHF</b>
<span class="dcl-price-month"> /{% trans "Month" %}
</span>
</p>
</h4>
{% endwith %}
{% else %}
<p><b>{% trans "Cores"%}</b>
<span class="pull-right">{{vm.cores}}</span>
</p>
<hr>
<p><b>{% trans "Memory"%}</b>
<span class="pull-right">{{vm.memory}} GB</span>
</p>
<hr>
<p><b>{% trans "Disk space"%}</b>
<span class="pull-right">{{vm.disk_size}} GB</span>
</p>
<hr>
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}}
CHF</b><span
class="dcl-price-month"> /{% trans "Month" %}</span>
</p></h4>
{% endif %}
</div> </div>
<br/> <br/>
{% url 'hosting:payment' as payment_url %} {% if not order %}
{% if payment_url in request.META.HTTP_REFERER %} <form method="post" id="virtual_machine_create_form">
<div class=" content pull-right"> {% csrf_token %}
<a href="{% url 'hosting:virtual_machines'%}" ><button class="btn btn-info">{% trans "Finish Configuration"%}</button></a> <div class="row">
<div class="col-sm-8">
<p class="dcl-place-order-text">{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.</p>
</div> </div>
<div class="col-sm-4 content">
<button class="btn btn-info pull-right"
id="btn-create-vm"
data-href="{% url 'hosting:order-confirmation' %}"
data-toggle="modal"
data-target="#createvm-modal">
{% trans "Place order"%}
</button>
</div>
</div>
</form>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Create VM Modal -->
<div class="modal fade" id="createvm-modal" tabindex="-1" role="dialog"
aria-hidden="true" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close hidden" data-dismiss="modal"
aria-label="create-vm-close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="modal-icon">
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<span class="sr-only">{% trans "Processing..." %}</span>
</div>
<h4 class="modal-title" id="createvm-modal-title">
</h4>
<div class="modal-text" id="createvm-modal-body">
{% trans "Hold tight, we are processing your request" %}
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</div>
<!-- / Create VM Modal -->
<script type="text/javascript"> <script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}.';
window.onload = function () { window.onload = function () {
var locale_date = moment.utc(document.getElementById("order-created_at").textContent, 'YYYY-MM-DD HH:mm').toDate(); var locale_date = moment.utc(document.getElementById("order-created_at").textContent, 'YYYY-MM-DD HH:mm').toDate();

View file

@ -11,7 +11,6 @@ from .views import (
SSHKeyChoiceView, DashboardView, SettingsView) SSHKeyChoiceView, DashboardView, SettingsView)
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'),
@ -22,9 +21,13 @@ urlpatterns = [
url(r'payment/?$', PaymentVMView.as_view(), name='payment'), url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
url(r'settings/?$', SettingsView.as_view(), name='settings'), url(r'settings/?$', SettingsView.as_view(), name='settings'),
url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'), url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(),
name='order-confirmation'),
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(),
name='orders'),
url(r'bills/?$', HostingBillListView.as_view(), name='bills'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(), name='bills'), url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(),
name='bills'),
url(r'cancel_order/(?P<pk>\d+)/?$', url(r'cancel_order/(?P<pk>\d+)/?$',
OrdersHostingDeleteView.as_view(), name='delete_order'), OrdersHostingDeleteView.as_view(), name='delete_order'),
url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(), url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(),
@ -41,13 +44,16 @@ urlpatterns = [
name='delete_ssh_key'), name='delete_ssh_key'),
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(), url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
name='create_ssh_key'), name='create_ssh_key'),
url(r'^notifications/$', NotificationsView.as_view(), name='notifications'), url(r'^notifications/$', NotificationsView.as_view(),
name='notifications'),
url(r'^notifications/(?P<pk>\d+)/?$', MarkAsReadNotificationView.as_view(), url(r'^notifications/(?P<pk>\d+)/?$', MarkAsReadNotificationView.as_view(),
name='read_notification'), name='read_notification'),
url(r'login/?$', LoginView.as_view(), name='login'), url(r'login/?$', LoginView.as_view(), name='login'),
url(r'signup/?$', SignupView.as_view(), name='signup'), url(r'signup/?$', SignupView.as_view(), name='signup'),
url(r'signup-validate/?$', SignupValidateView.as_view(), name='signup-validate'), url(r'signup-validate/?$', SignupValidateView.as_view(),
url(r'reset-password/?$', PasswordResetView.as_view(), name='reset_password'), name='signup-validate'),
url(r'reset-password/?$', PasswordResetView.as_view(),
name='reset_password'),
url(r'reset-password-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', url(r'reset-password-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
PasswordResetConfirmView.as_view(), name='reset_password_confirm'), PasswordResetConfirmView.as_view(), name='reset_password_confirm'),
url(r'^logout/?$', auth_views.logout, url(r'^logout/?$', auth_views.logout,

View file

@ -1,3 +1,5 @@
import json
import logging
import uuid import uuid
from django.conf import settings from django.conf import settings
@ -7,21 +9,22 @@ from django.contrib.auth.tokens import default_token_generator
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse_lazy, reverse from django.core.urlresolvers import reverse_lazy, reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.shortcuts import render from django.shortcuts import render
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
from django.views.generic import View, CreateView, FormView, ListView, \ from django.views.generic import View, CreateView, FormView, ListView, \
DetailView, \ DetailView, \
DeleteView, TemplateView, UpdateView DeleteView, TemplateView, UpdateView
from guardian.mixins import PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from oca.pool import WrongNameError, WrongIdError from oca.pool import WrongIdError
from stored_messages.api import mark_read from stored_messages.api import mark_read
from stored_messages.models import Message from stored_messages.models import Message
from stored_messages.settings import stored_messages_settings from stored_messages.settings import stored_messages_settings
from datacenterlight.tasks import create_vm_task
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import VirtualMachineSerializer, \ from opennebula_api.serializers import VirtualMachineSerializer, \
@ -37,8 +40,11 @@ from .forms import HostingUserSignupForm, HostingUserLoginForm, \
from .mixins import ProcessVMSelectionMixin from .mixins import ProcessVMSelectionMixin
from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey
CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a backend \ logger = logging.getLogger(__name__)
connection error. please try again in a few minutes."
CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a \
backend connection error. please try again in a few \
minutes."
class DashboardView(View): class DashboardView(View):
@ -369,17 +375,14 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
owner = self.request.user owner = self.request.user
manager = OpenNebulaManager() manager = OpenNebulaManager(
email=owner.email,
password=owner.password
)
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
# Add ssh key to user manager.manage_public_key([{'value': public_key, 'state': False}])
try:
manager.remove_public_key(user=owner, public_key=public_key)
except ConnectionError:
pass
except WrongNameError:
pass
return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs) return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs)
@ -420,6 +423,13 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
user=request.user, public_key=public_key, name=name) user=request.user, public_key=public_key, name=name)
filename = name + '_' + str(uuid.uuid4())[:8] + '_private.pem' filename = name + '_' + str(uuid.uuid4())[:8] + '_private.pem'
ssh_key.private_key.save(filename, content) ssh_key.private_key.save(filename, content)
owner = self.request.user
manager = OpenNebulaManager(
email=owner.email,
password=owner.password
)
public_key_str = public_key.decode()
manager.manage_public_key([{'value': public_key_str, 'state': True}])
return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar') return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar')
@ -464,23 +474,17 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView):
}) })
owner = self.request.user owner = self.request.user
manager = OpenNebulaManager() manager = OpenNebulaManager(
email=owner.email,
# Get user ssh key password=owner.password
public_key = str(form.cleaned_data.get('public_key', '')) )
# Add ssh key to user public_key = form.cleaned_data['public_key']
try: if type(public_key) is bytes:
manager.add_public_key( public_key = public_key.decode()
user=owner, public_key=public_key, merge=True) manager.manage_public_key([{'value': public_key, 'state': True}])
except ConnectionError:
pass
except WrongNameError:
pass
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
print(self.request.POST.dict())
form = self.get_form() form = self.get_form()
required = 'add_ssh' in self.request.POST required = 'add_ssh' in self.request.POST
form.fields['name'].required = required form.fields['name'].required = required
@ -603,23 +607,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
# Get billing address data # Get billing address data
billing_address_data = form.cleaned_data billing_address_data = form.cleaned_data
context = self.get_context_data()
template = request.session.get('template')
specs = request.session.get('specs')
vm_template_id = template.get('id', 1)
final_price = specs.get('price')
token = form.cleaned_data.get('token') token = form.cleaned_data.get('token')
owner = self.request.user owner = self.request.user
# Get or create stripe customer # Get or create stripe customer
customer = StripeCustomer.get_or_create(email=owner.email, customer = StripeCustomer.get_or_create(email=owner.email,
token=token) token=token)
@ -633,106 +624,18 @@ class PaymentVMView(LoginRequiredMixin, FormView):
# Create Billing Address # Create Billing Address
billing_address = form.save() billing_address = form.save()
request.session['billing_address_data'] = billing_address_data
# Make stripe charge to a customer request.session['billing_address'] = billing_address.id
stripe_utils = StripeUtils() request.session['token'] = token
charge_response = stripe_utils.make_charge(amount=final_price, request.session['customer'] = customer.id
customer=customer.stripe_id) return HttpResponseRedirect("{url}?{query_params}".format(
url=reverse('hosting:order-confirmation'),
# Check if the payment was approved
if not charge_response.get('response_object'):
msg = charge_response.get('error')
messages.add_message(
self.request, messages.ERROR, msg,
extra_tags='make_charge_error')
return HttpResponseRedirect(
reverse('hosting:payment') + '#payment_error')
charge = charge_response.get('response_object')
# Create OpenNebulaManager
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# Get user ssh key
if not UserHostingKey.objects.filter(
user=self.request.user).exists():
context.update({
'sshError': 'error',
'form': form
})
return render(request, self.template_name, context)
# For now just get first one
user_key = UserHostingKey.objects.filter(
user=self.request.user).first()
# Create a vm using logged user
vm_id = manager.create_vm(
template_id=vm_template_id,
# XXX: Confi
specs=specs,
ssh_key=user_key.public_key,
)
# Create a Hosting Order
order = HostingOrder.create(
price=final_price,
vm_id=vm_id,
customer=customer,
billing_address=billing_address
)
# Create a Hosting Bill
HostingBill.create(
customer=customer, billing_address=billing_address)
# Create Billing Address for User if he does not have one
if not customer.user.billing_addresses.count():
billing_address_data.update({
'user': customer.user.id
})
billing_address_user_form = UserBillingAddressForm(
billing_address_data)
billing_address_user_form.is_valid()
billing_address_user_form.save()
# Associate an order with a stripe payment
order.set_stripe_charge(charge)
# If the Stripe payment was successed, set order status approved
order.set_approved()
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
# Send notification to the user as soon as VM has been booked
context = {
'vm': vm,
'order': order,
'base_url': "{0}://{1}".format(request.scheme,
request.get_host()),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')}
}
email_data = {
'subject': context.get('page_header'),
'to': request.user.email,
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
return HttpResponseRedirect(
"{url}?{query_params}".format(
url=reverse('hosting:orders', kwargs={'pk': order.id}),
query_params='page=payment')) query_params='page=payment'))
else: else:
return self.form_invalid(form) return self.form_invalid(form)
class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin, class OrdersHostingDetailView(LoginRequiredMixin,
DetailView): DetailView):
template_name = "hosting/order_detail.html" template_name = "hosting/order_detail.html"
context_object_name = "order" context_object_name = "order"
@ -740,18 +643,42 @@ class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin,
permission_required = ['view_hostingorder'] permission_required = ['view_hostingorder']
model = HostingOrder model = HostingOrder
def get_object(self):
return HostingOrder.objects.filter(
pk=self.kwargs.get('pk')) if self.kwargs.get('pk') else None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Get context # Get context
context = super(DetailView, self).get_context_data(**kwargs) context = super(DetailView, self).get_context_data(**kwargs)
obj = self.get_object() obj = self.get_object()
owner = self.request.user owner = self.request.user
manager = OpenNebulaManager(email=owner.email, if 'specs' not in self.request.session:
password=owner.password) return HttpResponseRedirect(
reverse('hosting:create_virtual_machine'))
if 'token' not in self.request.session:
return HttpResponseRedirect(reverse('hosting:payment'))
stripe_customer_id = self.request.session.get('customer')
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
stripe_utils = StripeUtils()
card_details = stripe_utils.get_card_details(customer.stripe_id,
self.request.session.get(
'token'))
if not card_details.get('response_object'):
msg = card_details.get('error')
messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment')
return HttpResponseRedirect(
reverse('hosting:payment') + '#payment_error')
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')
else: else:
context['page_header_text'] = _('Invoice') context['page_header_text'] = _('Invoice')
if obj is not None:
try: try:
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
vm = manager.get_vm(obj.vm_id) vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data context['vm'] = VirtualMachineSerializer(vm).data
except WrongIdError: except WrongIdError:
@ -763,11 +690,98 @@ class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin,
context['error'] = 'WrongIdError' context['error'] = 'WrongIdError'
except ConnectionRefusedError: except ConnectionRefusedError:
messages.error(self.request, messages.error(self.request,
_( 'In order to create a VM, you need to create/upload your SSH KEY first.'
'In order to create a VM, you need to create/upload your SSH KEY first.')
) )
else:
context['site_url'] = reverse('hosting:create_virtual_machine')
context['cc_last4'] = card_details.get('response_object').get(
'last4')
context['cc_brand'] = card_details.get('response_object').get(
'cc_brand')
return context return context
def post(self, request):
template = request.session.get('template')
specs = request.session.get('specs')
stripe_customer_id = request.session.get('customer')
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
billing_address_data = request.session.get('billing_address_data')
billing_address_id = request.session.get('billing_address')
vm_template_id = template.get('id', 1)
# Make stripe charge to a customer
stripe_utils = StripeUtils()
card_details = stripe_utils.get_card_details(customer.stripe_id,
request.session.get(
'token'))
if not card_details.get('response_object'):
msg = card_details.get('error')
messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment')
return HttpResponseRedirect(
reverse('datacenterlight:payment') + '#payment_error')
card_details_dict = card_details.get('response_object')
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6)
plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(
cpu=cpu,
memory=memory,
disk_size=disk_size)
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
ram=memory,
ssd=disk_size,
version=1,
app='dcl')
stripe_plan = stripe_utils.get_or_create_stripe_plan(
amount=amount_to_be_charged,
name=plan_name,
stripe_plan_id=stripe_plan_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
customer.stripe_id,
[{"plan": stripe_plan.get(
'response_object').stripe_plan_id}])
stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active
if stripe_subscription_obj is None or stripe_subscription_obj.status != 'active':
msg = subscription_result.get('error')
messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment')
return HttpResponseRedirect(
reverse('hosting:payment') + '#payment_error')
user = {
'name': self.request.user.name,
'email': self.request.user.email,
'pass': self.request.user.password,
'request_scheme': request.scheme,
'request_host': request.get_host(),
'language': get_language(),
}
create_vm_task.delay(vm_template_id, user, specs, template,
stripe_customer_id, billing_address_data,
billing_address_id,
stripe_subscription_obj, card_details_dict)
for session_var in ['specs', 'template', 'billing_address',
'billing_address_data',
'token', 'customer']:
if session_var in request.session:
del request.session[session_var]
response = {
'status': True,
'redirect': reverse('hosting:virtual_machines'),
'msg_title': str(_('Thank you for the order.')),
'msg_body': str(_('Your VM will be up and running in a few moments.'
' We will send you a confirmation email as soon as'
' it is ready.'))
}
return HttpResponse(json.dumps(response),
content_type="application/json")
class OrdersHostingListView(LoginRequiredMixin, ListView): class OrdersHostingListView(LoginRequiredMixin, ListView):
template_name = "hosting/orders.html" template_name = "hosting/orders.html"
@ -918,7 +932,8 @@ class VirtualMachineView(LoginRequiredMixin, View):
'order': HostingOrder.objects.get( 'order': HostingOrder.objects.get(
vm_id=serializer.data['vm_id']) vm_id=serializer.data['vm_id'])
} }
except: except Exception as ex:
logger.debug("Exception generated {}".format(str(ex)))
pass pass
return render(request, self.template_name, context) return render(request, self.template_name, context)

View file

@ -1,13 +1,14 @@
import oca
import socket
import logging import logging
import socket
from oca.pool import WrongNameError, WrongIdError import oca
from oca.exceptions import OpenNebulaException
from django.conf import settings from django.conf import settings
from oca.exceptions import OpenNebulaException
from oca.pool import WrongNameError, WrongIdError
from hosting.models import HostingOrder
from utils.models import CustomUser from utils.models import CustomUser
from utils.tasks import save_ssh_key, save_ssh_key_error_handler
from .exceptions import KeyExistsError, UserExistsError, UserCredentialError from .exceptions import KeyExistsError, UserExistsError, UserCredentialError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,7 +18,8 @@ class OpenNebulaManager():
"""This class represents an opennebula manager.""" """This class represents an opennebula manager."""
def __init__(self, email=None, password=None): def __init__(self, email=None, password=None):
self.email = email
self.password = password
# Get oneadmin client # Get oneadmin client
self.oneadmin_client = self._get_opennebula_client( self.oneadmin_client = self._get_opennebula_client(
settings.OPENNEBULA_USERNAME, settings.OPENNEBULA_USERNAME,
@ -122,14 +124,17 @@ class OpenNebulaManager():
except WrongNameError: except WrongNameError:
user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'], user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'],
user.email, user.password, 'core') user.email, user.password,
logger.debug('Created a user for CustomObject: {user} with user id = {u_id}', 'core')
logger.debug(
'Created a user for CustomObject: {user} with user id = {u_id}',
user=user, user=user,
u_id=user_id u_id=user_id
) )
return user_id return user_id
except ConnectionRefusedError: except ConnectionRefusedError:
logger.error('Could not connect to host: {host} via protocol {protocol}'.format( logger.error(
'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
) )
@ -141,7 +146,8 @@ class OpenNebulaManager():
opennebula_user = user_pool.get_by_name(email) opennebula_user = user_pool.get_by_name(email)
return opennebula_user return opennebula_user
except WrongNameError as wrong_name_err: except WrongNameError as wrong_name_err:
opennebula_user = self.oneadmin_client.call(oca.User.METHODS['allocate'], email, opennebula_user = self.oneadmin_client.call(
oca.User.METHODS['allocate'], email,
password, 'core') password, 'core')
logger.debug( logger.debug(
"User {0} does not exist. Created the user. User id = {1}", "User {0} does not exist. Created the user. User id = {1}",
@ -150,7 +156,8 @@ class OpenNebulaManager():
) )
return opennebula_user return opennebula_user
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info('Could not connect to host: {host} via protocol {protocol}'.format( logger.info(
'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
) )
@ -161,7 +168,8 @@ class OpenNebulaManager():
user_pool = oca.UserPool(self.oneadmin_client) user_pool = oca.UserPool(self.oneadmin_client)
user_pool.info() user_pool.info()
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info('Could not connect to host: {host} via protocol {protocol}'.format( logger.info(
'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
) )
@ -183,7 +191,8 @@ class OpenNebulaManager():
raise ConnectionRefusedError raise ConnectionRefusedError
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info('Could not connect to host: {host} via protocol {protocol}'.format( logger.info(
'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
) )
@ -208,6 +217,33 @@ class OpenNebulaManager():
except: except:
raise ConnectionRefusedError raise ConnectionRefusedError
def get_primary_ipv4(self, vm_id):
"""
Returns the primary IPv4 of the given vm.
To be changed later.
:return: An IP address string, if it exists else returns None
"""
all_ipv4s = self.get_vm_ipv4_addresses(vm_id)
if len(all_ipv4s) > 0:
return all_ipv4s[0]
else:
return None
def get_vm_ipv4_addresses(self, vm_id):
"""
Returns a list of IPv4 addresses of the given vm
:param vm_id: The ID of the vm
:return:
"""
ipv4s = []
vm = self.get_vm(vm_id)
for nic in vm.template.nics:
if hasattr(nic, 'ip'):
ipv4s.append(nic.ip)
return ipv4s
def create_vm(self, template_id, specs, ssh_key=None, vm_name=None): def create_vm(self, template_id, specs, ssh_key=None, vm_name=None):
template = self.get_template(template_id) template = self.get_template(template_id)
@ -258,7 +294,8 @@ class OpenNebulaManager():
vm_specs += "<CONTEXT>" vm_specs += "<CONTEXT>"
if ssh_key: if ssh_key:
vm_specs += "<SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>".format(ssh=ssh_key) vm_specs += "<SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>".format(
ssh=ssh_key)
vm_specs += """<NETWORK>YES</NETWORK> vm_specs += """<NETWORK>YES</NETWORK>
</CONTEXT> </CONTEXT>
</TEMPLATE> </TEMPLATE>
@ -312,7 +349,9 @@ class OpenNebulaManager():
template_pool.info() template_pool.info()
return template_pool return template_pool
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info('Could not connect to host: {host} via protocol {protocol}'.format( logger.info(
"""Could not connect to host: {host} via protocol
{protocol}""".format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
) )
@ -347,7 +386,8 @@ class OpenNebulaManager():
except: except:
raise ConnectionRefusedError raise ConnectionRefusedError
def create_template(self, name, cores, memory, disk_size, core_price, memory_price, def create_template(self, name, cores, memory, disk_size, core_price,
memory_price,
disk_size_price, ssh=''): disk_size_price, ssh=''):
"""Create and add a new template to opennebula. """Create and add a new template to opennebula.
:param name: A string representation describing the template. :param name: A string representation describing the template.
@ -490,3 +530,57 @@ class OpenNebulaManager():
except ConnectionError: except ConnectionError:
raise raise
def manage_public_key(self, keys, hosts=None, countdown=0):
"""
A function that manages the supplied keys in the
authorized_keys file of the given list of hosts. If hosts
parameter is not supplied, all hosts of this customer
will be configured with the supplied keys
:param keys: A list of ssh keys that are to be added/removed
A key should be a dict of the form
{
'value': 'sha-.....', # public key as string
'state': True # whether key is to be added or
} # removed
:param hosts: A list of hosts IP addresses
:param countdown: Parameter to be passed to celery apply_async
Allows to delay a task by `countdown` number of seconds
:return:
"""
if hosts is None:
hosts = self.get_all_hosts()
if len(hosts) > 0 and len(keys) > 0:
save_ssh_key.apply_async((hosts, keys), countdown=countdown,
link_error=save_ssh_key_error_handler.s())
else:
logger.debug(
"Keys and/or hosts are empty, so not managing any keys")
def get_all_hosts(self):
"""
A utility function to obtain all hosts of this owner
:return: A list of hosts IP addresses, empty if none exist
"""
owner = CustomUser.objects.filter(
email=self.email).first()
all_orders = HostingOrder.objects.filter(customer__user=owner)
hosts = []
if len(all_orders) > 0:
logger.debug("The user {} has 1 or more VMs. We need to configure "
"the ssh keys.".format(self.email))
for order in all_orders:
try:
vm = self.get_vm(order.vm_id)
for nic in vm.template.nics:
if hasattr(nic, 'ip'):
hosts.append(nic.ip)
except WrongIdError:
logger.debug(
"VM with ID {} does not exist".format(order.vm_id))
else:
logger.debug("The user {} has no VMs. We don't need to configure "
"the ssh keys.".format(self.email))
return hosts

View file

@ -96,3 +96,5 @@ pyflakes==1.5.0
billiard==3.5.0.3 billiard==3.5.0.3
amqp==2.2.1 amqp==2.2.1
vine==1.1.4 vine==1.1.4
#git+https://github.com/ungleich/cdist.git#egg=cdist
file:///home/app/cdist#egg=cdist

11
utils/hosting_utils.py Normal file
View file

@ -0,0 +1,11 @@
from hosting.models import UserHostingKey
def get_all_public_keys(customer):
"""
Returns all the public keys of the user
:param customer: The customer whose public keys are needed
:return: A list of public keys
"""
return UserHostingKey.objects.filter(user_id=customer.id).values_list(
"public_key", flat=True)

View file

@ -1,8 +1,15 @@
import tempfile
import cdist
from cdist.integration import configure_hosts_simple
from celery.result import AsyncResult
from celery import current_task
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from django.conf import settings from django.conf import settings
from dynamicweb.celery import app
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from dynamicweb.celery import app
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
@ -18,3 +25,74 @@ def send_plain_email_task(self, email_data):
""" """
email = EmailMessage(**email_data) email = EmailMessage(**email_data)
email.send() email.send()
@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
def save_ssh_key(self, hosts, keys):
"""
Saves ssh key into the VMs of a user using cdist
:param hosts: A list of hosts to be configured
:param keys: A list of keys to be added. A key should be dict of the
form {
'value': 'sha-.....', # public key as string
'state': True # whether key is to be added or
} # removed
"""
logger.debug(
"Running save_ssh_key on {}".format(current_task.request.hostname))
logger.debug("""Running save_ssh_key task for
Hosts: {hosts_str}
Keys: {keys_str}""".format(hosts_str=", ".join(hosts),
keys_str=", ".join([
"{value}->{state}".format(
value=key.get('value'),
state=str(
key.get('state')))
for key in keys]))
)
return_value = True
with tempfile.NamedTemporaryFile(delete=True) as tmp_manifest:
# Generate manifest to be used for configuring the hosts
lines_list = [
' --key "{key}" --state {state} \\\n'.format(
key=key['value'],
state='present' if key['state'] else 'absent'
).encode('utf-8')
for key in keys]
lines_list.insert(0, b'__ssh_authorized_keys root \\\n')
tmp_manifest.writelines(lines_list)
tmp_manifest.flush()
try:
configure_hosts_simple(hosts,
tmp_manifest.name,
verbose=cdist.argparse.VERBOSE_TRACE)
except Exception as cdist_exception:
logger.error(cdist_exception)
return_value = False
email_data = {
'subject': "celery save_ssh_key error - task id {0}".format(
self.request.id.__str__()),
'from_email': current_task.request.hostname,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format(
self.request.id.__str__(), False, str(cdist_exception)),
}
send_plain_email_task(email_data)
return return_value
@app.task
def save_ssh_key_error_handler(uuid):
result = AsyncResult(uuid)
exc = result.get(propagate=False)
logger.error('Task {0} raised exception: {1!r}\n{2!r}'.format(
uuid, exc, result.traceback))
email_data = {
'subject': "[celery error] Save SSH key error {0}".format(uuid),
'from_email': current_task.request.hostname,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format(
uuid, exc, result.traceback),
}
send_plain_email_task(email_data)

View file

@ -1,16 +1,20 @@
import uuid import uuid
from time import sleep
from unittest.mock import patch from unittest.mock import patch
import stripe import stripe
from celery.result import AsyncResult
from django.conf import settings
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.test import Client from django.test import Client
from django.test import TestCase from django.test import TestCase, override_settings
from unittest import skipIf
from model_mommy import mommy from model_mommy import mommy
from datacenterlight.models import StripePlan from datacenterlight.models import StripePlan
from membership.models import StripeCustomer from membership.models import StripeCustomer
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from django.conf import settings from .tasks import save_ssh_key
class BaseTestCase(TestCase): class BaseTestCase(TestCase):
@ -235,3 +239,57 @@ class StripePlanTestCase(TestStripeCustomerDescription):
'response_object').stripe_plan_id}]) 'response_object').stripe_plan_id}])
self.assertIsNone(result.get('response_object'), None) self.assertIsNone(result.get('response_object'), None)
self.assertIsNotNone(result.get('error')) self.assertIsNotNone(result.get('error'))
class SaveSSHKeyTestCase(TestCase):
"""
A test case to test the celery save_ssh_key task
"""
@override_settings(
task_eager_propagates=True,
task_always_eager=True,
)
def setUp(self):
self.public_key = settings.TEST_MANAGE_SSH_KEY_PUBKEY
self.hosts = settings.TEST_MANAGE_SSH_KEY_HOST
@skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or
settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or
settings.TEST_MANAGE_SSH_KEY_HOST is None or
settings.TEST_MANAGE_SSH_KEY_HOST is "",
"""Skipping test_save_ssh_key_add because either host
or public key were not specified or were empty""")
def test_save_ssh_key_add(self):
async_task = save_ssh_key.delay([self.hosts],
[{'value': self.public_key,
'state': True}])
save_ssh_key_result = None
for i in range(0, 10):
sleep(5)
res = AsyncResult(async_task.task_id)
if type(res.result) is bool:
save_ssh_key_result = res.result
break
self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None")
self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False")
@skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or
settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or
settings.TEST_MANAGE_SSH_KEY_HOST is None or
settings.TEST_MANAGE_SSH_KEY_HOST is "",
"""Skipping test_save_ssh_key_add because either host
or public key were not specified or were empty""")
def test_save_ssh_key_remove(self):
async_task = save_ssh_key.delay([self.hosts],
[{'value': self.public_key,
'state': False}])
save_ssh_key_result = None
for i in range(0, 10):
sleep(5)
res = AsyncResult(async_task.task_id)
if type(res.result) is bool:
save_ssh_key_result = res.result
break
self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None")
self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False")