commit
beb0a8926c
9 changed files with 149 additions and 105 deletions
|
@ -4,8 +4,9 @@ from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from utils.mailer import BaseEmail
|
from utils.mailer import BaseEmail
|
||||||
|
|
||||||
from .models import HostingOrder, HostingBill
|
from .models import HostingOrder, HostingBill, HostingPlan
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(HostingOrder)
|
admin.site.register(HostingOrder)
|
||||||
admin.site.register(HostingBill)
|
admin.site.register(HostingBill)
|
||||||
|
admin.site.register(HostingPlan)
|
||||||
|
|
24
hosting/migrations/0040_hostingplan.py
Normal file
24
hosting/migrations/0040_hostingplan.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.4 on 2017-05-13 11:35
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('hosting', '0039_hostingorder_price'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HostingPlan',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('disk_size', models.FloatField(default=0.0)),
|
||||||
|
('cpu_cores', models.FloatField(default=0.0)),
|
||||||
|
('memory', models.FloatField(default=0.0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -21,6 +21,31 @@ from .managers import VMPlansManager
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HostingPlan(models.Model):
|
||||||
|
disk_size = models.FloatField(default=0.0)
|
||||||
|
cpu_cores = models.FloatField(default=0.0)
|
||||||
|
memory = models.FloatField(default=0.0)
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'cpu':self.cpu_cores,
|
||||||
|
'memory': self.memory,
|
||||||
|
'disk_size': self.disk_size,
|
||||||
|
'price': self.price(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_serialized_configs(cls):
|
||||||
|
return [cfg.serialize()
|
||||||
|
for cfg in cls.objects.all()]
|
||||||
|
|
||||||
|
def price(self):
|
||||||
|
price = self.disk_size * 0.2
|
||||||
|
price += self.cpu_cores * 5
|
||||||
|
price += self.memory * 2
|
||||||
|
return price
|
||||||
|
|
||||||
class HostingOrder(AssignPermissionsMixin, models.Model):
|
class HostingOrder(AssignPermissionsMixin, models.Model):
|
||||||
|
|
||||||
ORDER_APPROVED_STATUS = 'Approved'
|
ORDER_APPROVED_STATUS = 'Approved'
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="orders-container">
|
<div class="orders-container" style="padding-bottom: 15%">
|
||||||
{# Adress bar #}
|
{# Adress bar #}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="invoice-title">
|
<div class="invoice-title">
|
||||||
|
@ -78,14 +78,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% trans "CH02 0900 0000 6071 8848 8%}
|
{% trans "CH02 0900 0000 6071 8848 8" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% trans "POFICHBEXXX" %}
|
{% trans "POFICHBEXXX" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -11,26 +11,22 @@
|
||||||
<form method="POST" action="">
|
<form method="POST" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
Select VM:
|
Select VM Template:
|
||||||
<select name="vm_template_id">
|
<select name="vm_template_id">
|
||||||
{% for template in templates %}
|
{% for template in templates %}
|
||||||
|
<option value="{{template.id}}">{{template.name}} </option>
|
||||||
<option value="{{template.id}}">
|
|
||||||
CORE: {{template.cores}},
|
|
||||||
RAM: {{template.memory}} GiB,
|
|
||||||
SSD: {{template.disk_size}} GiB
|
|
||||||
</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
Select VM Configuration:
|
Select VM Configuration:
|
||||||
<select name="vm_image_id">
|
<select name="configuration">
|
||||||
{% for image in images %}
|
{% for config in configuration_options %}
|
||||||
|
<option value="{{config.id}}">
|
||||||
<option value="{{image.id}}">{{image.name}} </option>
|
CORE: {{config.cpu|floatformat}},
|
||||||
|
RAM: {{config.memory|floatformat}} GiB,
|
||||||
|
SSD: {{config.disk_size|floatformat}} GiB
|
||||||
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -100,14 +100,17 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<!-- <p><b>Type</b> <span class="pull-right">{{request.session.vm_specs.location_code}}</span></p> -->
|
<!-- <p><b>Type</b> <span class="pull-right">{{request.session.vm_specs.location_code}}</span></p> -->
|
||||||
<!-- <hr> -->
|
<!-- <hr> -->
|
||||||
<p><b>Cores</b> <span class="pull-right">{{request.session.template.cores}}</span></p>
|
<p><b>Cores</b> <span
|
||||||
|
class="pull-right">{{request.session.specs.cpu|floatformat}}</span></p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><b>Memory</b> <span class="pull-right">{{request.session.template.memory}} GiB</span></p>
|
<p><b>Memory</b> <span
|
||||||
|
class="pull-right">{{request.session.specs.memory|floatformat}} GiB</span></p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><b>Disk space</b> <span class="pull-right">{{request.session.template.disk_size}} GiB</span></p>
|
<p><b>Disk space</b> <span
|
||||||
|
class="pull-right">{{request.session.specs.disk_size|floatformat}} GiB</span></p>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>Total<p
|
<h4>Total<p
|
||||||
class="pull-right"><b>{{request.session.template.price }} CHF</b></p></h4>
|
class="pull-right"><b>{{request.session.specs.price }} CHF</b></p></h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,14 +25,13 @@ from utils.stripe_utils import StripeUtils
|
||||||
from utils.forms import BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm
|
from utils.forms import BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm
|
||||||
from utils.views import PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin
|
from utils.views import PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin
|
||||||
from utils.mailer import BaseEmail
|
from utils.mailer import BaseEmail
|
||||||
from .models import HostingOrder, HostingBill, UserHostingKey
|
from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey
|
||||||
from .forms import HostingUserSignupForm, HostingUserLoginForm, UserHostingKeyForm
|
from .forms import HostingUserSignupForm, HostingUserLoginForm, UserHostingKeyForm
|
||||||
from .mixins import ProcessVMSelectionMixin
|
from .mixins import ProcessVMSelectionMixin
|
||||||
|
|
||||||
from opennebula_api.models import OpenNebulaManager
|
from opennebula_api.models import OpenNebulaManager
|
||||||
from opennebula_api.serializers import VirtualMachineSerializer,\
|
from opennebula_api.serializers import VirtualMachineSerializer,\
|
||||||
VirtualMachineTemplateSerializer,\
|
VirtualMachineTemplateSerializer
|
||||||
ImageSerializer
|
|
||||||
|
|
||||||
|
|
||||||
from oca.exceptions import OpenNebulaException
|
from oca.exceptions import OpenNebulaException
|
||||||
|
@ -391,12 +390,12 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
||||||
|
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
|
|
||||||
specifications = request.session.get('template')
|
template = request.session.get('template')
|
||||||
|
specs = request.session.get('specs')
|
||||||
|
|
||||||
vm_template_id = specifications.get('id', 1)
|
vm_template_id = template.get('id', 1)
|
||||||
vm_image_id = request.session.get('image').get('id', 1)
|
|
||||||
|
|
||||||
final_price = specifications.get('price', 1)
|
final_price = specs.get('price')
|
||||||
|
|
||||||
token = form.cleaned_data.get('token')
|
token = form.cleaned_data.get('token')
|
||||||
|
|
||||||
|
@ -440,11 +439,13 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
||||||
except UserHostingKey.DoesNotExist:
|
except UserHostingKey.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Create a vm using logged user
|
# Create a vm using logged user
|
||||||
vm_id = manager.create_vm(
|
vm_id = manager.create_vm(
|
||||||
template_id=vm_template_id,
|
template_id=vm_template_id,
|
||||||
|
#XXX: Confi
|
||||||
|
specs=specs,
|
||||||
ssh_key=user_key.public_key,
|
ssh_key=user_key.public_key,
|
||||||
image_id=vm_image_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a Hosting Order
|
# Create a Hosting Order
|
||||||
|
@ -571,11 +572,11 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
manager = OpenNebulaManager()
|
manager = OpenNebulaManager()
|
||||||
templates = manager.get_templates()
|
templates = manager.get_templates()
|
||||||
images = manager.get_images()
|
configuration_options = HostingPlan.get_serialized_configs()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'templates': VirtualMachineTemplateSerializer(templates, many=True).data,
|
'templates': VirtualMachineTemplateSerializer(templates, many=True).data,
|
||||||
'images' : ImageSerializer(images, many=True).data
|
'configuration_options' : configuration_options,
|
||||||
}
|
}
|
||||||
return render(request, self.template_name, context)
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
@ -583,10 +584,11 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
||||||
manager = OpenNebulaManager()
|
manager = OpenNebulaManager()
|
||||||
template_id = request.POST.get('vm_template_id')
|
template_id = request.POST.get('vm_template_id')
|
||||||
template = manager.get_template(template_id)
|
template = manager.get_template(template_id)
|
||||||
image_id = request.POST.get('vm_image_id')
|
configuration_id = int(request.POST.get('configuration'))
|
||||||
image = manager.get_image(image_id)
|
configuration = HostingPlan.objects.get(id=configuration_id)
|
||||||
request.session['template'] = VirtualMachineTemplateSerializer(template).data
|
request.session['template'] = VirtualMachineTemplateSerializer(template).data
|
||||||
request.session['image'] = ImageSerializer(image).data
|
|
||||||
|
request.session['specs'] = configuration.serialize()
|
||||||
return redirect(reverse('hosting:payment'))
|
return redirect(reverse('hosting:payment'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -699,6 +701,10 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, DetailV
|
||||||
# Get vms
|
# Get vms
|
||||||
queryset = manager.get_vms()
|
queryset = manager.get_vms()
|
||||||
vms = VirtualMachineSerializer(queryset, many=True).data
|
vms = VirtualMachineSerializer(queryset, many=True).data
|
||||||
|
# Set total price
|
||||||
|
bill = context['bill']
|
||||||
|
bill.total_price = 0.0
|
||||||
|
for vm in vms:
|
||||||
|
bill.total_price += vm['price']
|
||||||
context['vms'] = vms
|
context['vms'] = vms
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -109,37 +109,64 @@ class OpenNebulaManager():
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_vm(self, template_id, image_id=None, ssh_key=None):
|
def create_template(self, name, cores, memory, disk_size, core_price, memory_price,
|
||||||
extra_template_formater = """<CONTEXT>
|
disk_size_price, ssh='' ):
|
||||||
<SSH_PUBLIC_KEY>{ssh_key}</SSH_PUBLIC_KEY>
|
"""Create and add a new template to opennebula.
|
||||||
</CONTEXT>
|
:param name: A string representation describing the template.
|
||||||
<DISK>
|
Used as label in view.
|
||||||
<IMAGE_ID>{image_id}</IMAGE_ID>
|
:param cores: Amount of virtual cpu cores for the VM.
|
||||||
</DISK>
|
:param memory: Amount of RAM for the VM (GB)
|
||||||
"""
|
:param disk_size: Amount of disk space for VM (GB)
|
||||||
|
:param core_price: Price of virtual cpu for the VM per core.
|
||||||
|
:param memory_price: Price of RAM for the VM per GB
|
||||||
|
:param disk_size_price: Price of disk space for VM per GB
|
||||||
|
:param ssh: User public ssh key
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_id = oca.VmTemplate.allocate(
|
||||||
|
self.oneadmin_client,
|
||||||
|
template_string_formatter.format(
|
||||||
|
name=name,
|
||||||
|
vcpu=cores,
|
||||||
|
cpu=0.1*cores,
|
||||||
|
size=1024 * disk_size,
|
||||||
|
memory=1024 * memory,
|
||||||
|
# * 10 because we set cpu to *0.1
|
||||||
|
cpu_cost=10*core_price,
|
||||||
|
memory_cost=memory_price,
|
||||||
|
disk_cost=disk_size_price,
|
||||||
|
ssh=ssh
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_vm(self, template_id, specs, ssh_key=None):
|
||||||
|
|
||||||
template = self.get_template(template_id)
|
template = self.get_template(template_id)
|
||||||
vm_id = template.instantiate(name ='', pending=False, extra_template='')
|
vm_specs_formatter = """<TEMPLATE>
|
||||||
image = self.get_image(image_id)
|
<MEMORY>{memory}</MEMORY>
|
||||||
|
<VCPU>{vcpu}</VCPU>
|
||||||
|
<CPU>{cpu}</CPU>
|
||||||
|
<DISK>
|
||||||
|
<TYPE>fs</TYPE>
|
||||||
|
<SIZE>{size}</SIZE>
|
||||||
|
<DEV_PREFIX>vd</DEV_PREFIX>
|
||||||
|
</DISK>
|
||||||
|
<CONTEXT>
|
||||||
|
<SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>
|
||||||
|
</CONTEXT>
|
||||||
|
</TEMPLATE>
|
||||||
|
"""
|
||||||
|
vm_id = template.instantiate(name ='',
|
||||||
|
pending=False,
|
||||||
|
extra_template=vm_specs_formatter.format(
|
||||||
|
vcpu=int(specs['cpu']),
|
||||||
|
cpu=0.1* int(specs['cpu']),
|
||||||
|
size=1024 * int(specs['disk_size']),
|
||||||
|
memory=1024 * int(specs['memory']),
|
||||||
|
ssh=ssh_key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
image_name = "{image_name}{template_name}{vm_id}".format(
|
|
||||||
image_name=image.name,
|
|
||||||
template_name=template.name,
|
|
||||||
vm_id = vm_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
image_id = image.clone(name=image_name)
|
|
||||||
|
|
||||||
self.oneadmin_client.call(
|
|
||||||
oca.VmTemplate.METHODS['update'],
|
|
||||||
vm_id,
|
|
||||||
extra_template_formater.format(
|
|
||||||
ssh_key=ssh_key,
|
|
||||||
image_id=image_id
|
|
||||||
),
|
|
||||||
# 0 = Replace / 1 = Merge
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
self.oneadmin_client.call(
|
self.oneadmin_client.call(
|
||||||
oca.VirtualMachine.METHODS['chown'],
|
oca.VirtualMachine.METHODS['chown'],
|
||||||
|
@ -262,38 +289,3 @@ class OpenNebulaManager():
|
||||||
self.opennebula_user.id,
|
self.opennebula_user.id,
|
||||||
new_password
|
new_password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_image_pool(self):
|
|
||||||
try:
|
|
||||||
image_pool = oca.ImagePool(self.oneadmin_client)
|
|
||||||
image_pool.info()
|
|
||||||
#TODO: Replace with logger
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
logger.info('Could not connect to host: {host} via protocol {protocol}'.format(
|
|
||||||
host=settings.OPENNEBULA_DOMAIN,
|
|
||||||
protocol=settings.OPENNEBULA_PROTOCOL)
|
|
||||||
)
|
|
||||||
raise ConnectionRefusedError
|
|
||||||
return image_pool
|
|
||||||
|
|
||||||
def get_images(self):
|
|
||||||
try:
|
|
||||||
public_images = [
|
|
||||||
image
|
|
||||||
for image in self._get_image_pool()
|
|
||||||
if 'public-' in image.name
|
|
||||||
]
|
|
||||||
return public_images
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
return []
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_image(self, image_id):
|
|
||||||
image_id = int(image_id)
|
|
||||||
try:
|
|
||||||
image_pool = self._get_image_pool()
|
|
||||||
return image_pool.get_by_id(image_id)
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .models import OpenNebulaManager
|
||||||
class VirtualMachineTemplateSerializer(serializers.Serializer):
|
class VirtualMachineTemplateSerializer(serializers.Serializer):
|
||||||
"""Serializer to map the virtual machine template instance into JSON format."""
|
"""Serializer to map the virtual machine template instance into JSON format."""
|
||||||
id = serializers.IntegerField(read_only=True)
|
id = serializers.IntegerField(read_only=True)
|
||||||
name = serializers.CharField()
|
name = serializers.SerializerMethodField()
|
||||||
cores = serializers.IntegerField(source='template.vcpu')
|
cores = serializers.IntegerField(source='template.vcpu')
|
||||||
disk = serializers.IntegerField(write_only=True)
|
disk = serializers.IntegerField(write_only=True)
|
||||||
disk_size = serializers.SerializerMethodField()
|
disk_size = serializers.SerializerMethodField()
|
||||||
|
@ -63,6 +63,9 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
|
||||||
def get_memory(self, obj):
|
def get_memory(self, obj):
|
||||||
return int(obj.template.memory)/1024
|
return int(obj.template.memory)/1024
|
||||||
|
|
||||||
|
def get_name(self, obj):
|
||||||
|
return obj.name
|
||||||
|
|
||||||
class VirtualMachineSerializer(serializers.Serializer):
|
class VirtualMachineSerializer(serializers.Serializer):
|
||||||
"""Serializer to map the virtual machine instance into JSON format."""
|
"""Serializer to map the virtual machine instance into JSON format."""
|
||||||
|
|
||||||
|
@ -116,9 +119,3 @@ class VirtualMachineSerializer(serializers.Serializer):
|
||||||
for disk in template.disks:
|
for disk in template.disks:
|
||||||
price += int(disk.size)/1024 * float(template.disk_cost)
|
price += int(disk.size)/1024 * float(template.disk_cost)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
class ImageSerializer(serializers.Serializer):
|
|
||||||
"""Serializer to map the image instance into JSON format."""
|
|
||||||
id = serializers.IntegerField(read_only=True)
|
|
||||||
name = serializers.CharField()
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue