Commit 4b1d3f50 by Guba Sándor

Merge branch 'issue-24' into feature-save-as

Conflicts:
	circle/dashboard/views.py
	circle/storage/models.py
parents bf9231ac 16d245c1
from datetime import timedelta
import uuid
from django.contrib.auth.models import User
from django.contrib.auth.forms import (
......@@ -20,7 +19,9 @@ from sizefield.widgets import FileSizeWidget
from firewall.models import Vlan, Host
from storage.models import Disk, DataStore
from vm.models import InstanceTemplate, Lease, InterfaceTemplate, Node
from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Instance
)
VLANS = Vlan.objects.all()
DISKS = Disk.objects.exclude(type="qcow2-snap")
......@@ -728,27 +729,61 @@ class LeaseForm(forms.ModelForm):
class DiskAddForm(forms.Form):
name = forms.CharField()
size = forms.CharField(widget=FileSizeWidget)
size = forms.CharField(widget=FileSizeWidget, required=False)
url = forms.CharField(required=False)
is_template = forms.CharField()
object_pk = forms.CharField()
def __init__(self, *args, **kwargs):
self.is_template = kwargs.pop("is_template")
self.object_pk = kwargs.pop("object_pk")
self.user = kwargs.pop("user")
super(DiskAddForm, self).__init__(*args, **kwargs)
self.initial['is_template'] = 1 if self.is_template is True else 0
self.initial['object_pk'] = self.object_pk
def clean_size(self):
size_in_bytes = self.cleaned_data.get("size")
if not size_in_bytes.isdigit():
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise forms.ValidationError(_("Invalid format, you can use "
" GB or MB!"))
return size_in_bytes
def save(self, vm, commit=True):
def clean(self):
cleaned_data = self.cleaned_data
size = cleaned_data.get("size")
url = cleaned_data.get("url")
if not size and not url:
msg = _("You have to either specify size or URL")
self._errors[_("Global")] = self.error_class([msg])
return cleaned_data
def save(self, commit=True):
data = self.cleaned_data
d = Disk(
name=data['name'],
filename=str(uuid.uuid4()),
datastore=DataStore.objects.all()[0],
type="qcow2-norm",
size=data['size'],
dev_num="a",
)
d.save()
vm.disks.add(d)
if self.is_template:
inst = InstanceTemplate.objects.get(pk=self.object_pk)
else:
inst = Instance.objects.get(pk=self.object_pk)
if data['size']:
kwargs = {
'name': data['name'],
'type': "qcow2-norm",
'datastore': DataStore.objects.all()[0],
'size': data['size'],
}
d = Disk.create_empty(instance=inst, user=self.user, **kwargs)
else:
kwargs = {
'name': data['name'],
'url': data['url'],
}
Disk.create_from_url_async(instance=inst, user=self.user,
**kwargs)
d = None
return d
@property
......@@ -756,11 +791,23 @@ class DiskAddForm(forms.Form):
helper = FormHelper()
helper.form_show_labels = False
helper.layout = Layout(
Field("is_template", type="hidden"),
Field("object_pk", type="hidden"),
Field("name", placeholder=_("Name")),
Field("size", placeholder=_("Disk size (for example: 20GB, "
"1500MB)")),
Field("url", placeholder=_("URL to an ISO image")),
AnyTag(
"div",
HTML(
_("Either specify the size for an empty disk or a URL "
"to an ISO image!")
),
css_class="alert alert-info",
style="padding: 5px; text-align: justify;",
),
)
helper.add_input(Submit("submit", "Create new disk",
helper.add_input(Submit("submit", _("Add"),
css_class="btn btn-success"))
return helper
......
{% load i18n %}
{% load sizefieldtags %}
<i class="{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% endif %}"></i>
{{ d.name }} (#{{ d.id }}) -
{% if not d.is_downloading %}
{% if d.ready %}
{{ d.size|filesize }}
{% else %}
<div class="label label-danger">failed</div>
{% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
<div class="btn btn-xs btn-danger pull-right"><i class="icon-remove"></i> Remove</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block content %}
......@@ -22,7 +23,7 @@
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h3>
<h4 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
......@@ -64,8 +65,35 @@
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-file"></i> {% trans "Disk list" %}</h4>
</div>
<div class="panel-body">
<ul style="list-style: none; padding-left: 0;">
{% for d in disks %}
<li>
{% include "dashboard/_disk-list-element.html" %}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-folder-open"></i> {% trans "Create new disk" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.disk-add" %}" method="POST">
{% crispy disk_add_form %}
</form>
</div>
</div>
</div>
</div><!-- .col-md-4 -->
</div><!-- .row -->
<style>
......
......@@ -52,12 +52,15 @@
</a>
</div>
</h3>
<div class="row" id="vm-details-disk-add-for-form">
</div>
<div class="row" id="vm-details-disk-add-for-form"></div>
{% if not instance.disks.all %}
{% trans "No disks are added!" %}
{% endif %}
{% for d in instance.disks.all %}
<h4 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-file"></i> {{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }}
{% include "dashboard/_disk-list-element.html" %}
</h4>
{% endfor %}
</div>
......@@ -67,7 +70,7 @@
<div class="col-md-12">
<div>
<hr />
<form method="POST" action="" style="max-width: 300px;">
<form method="POST" action="{% url "dashboard.views.disk-add" %}" style="max-width: 350px;">
{% crispy forms.disk_add_form %}
</form>
<hr />
......
......@@ -213,8 +213,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a",
'disk-size': 1})
response = c.post("/dashboard/disk/add/", {
'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 403)
self.assertEqual(disks, inst.disks.count())
......@@ -224,8 +228,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a",
'disk-size': 1})
response = c.post("/dashboard/disk/add/", {
'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 302)
self.assertEqual(disks + 1, inst.disks.count())
......
......@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, VmDetailVncTokenView,
VmMigrateView, DiskAddView, VmDetailVncTokenView,
)
urlpatterns = patterns(
......@@ -89,4 +89,7 @@ urlpatterns = patterns(
url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"),
url(r'^disk/add/$', DiskAddView.as_view(),
name="dashboard.views.disk-add"),
)
......@@ -22,7 +22,7 @@ from django.views.generic import (TemplateView, DetailView, View, DeleteView,
UpdateView, CreateView)
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.template.defaultfilters import title
from django.template.defaultfilters import title as title_filter
from django.template.loader import render_to_string
from django.forms.models import inlineformset_factory
......@@ -196,7 +196,10 @@ class VmDetailView(CheckedDetailView):
).all()
context['acl'] = get_vm_acl_data(instance)
context['forms'] = {
'disk_add_form': DiskAddForm(prefix="disk"),
'disk_add_form': DiskAddForm(
user=self.request.user,
is_template=False, object_pk=self.get_object().pk,
prefix="disk"),
}
context['os_type_icon'] = instance.os_type.replace("unknown",
"question")
......@@ -215,7 +218,6 @@ class VmDetailView(CheckedDetailView):
'port': self.__add_port,
'new_network_vlan': self.__new_network,
'save_as': self.__save_as,
'disk-name': self.__add_disk,
'shut_down': self.__shut_down,
'sleep': self.__sleep,
'wake_up': self.__wake_up,
......@@ -390,24 +392,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.template-detail",
kwargs={'pk': template.pk}))
def __add_disk(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
form = DiskAddForm(request.POST, prefix="disk")
if form.is_valid():
messages.success(request, _("New disk successfully created!"))
form.save(self.object)
else:
error = "<br /> ".join(["<strong>%s</strong>: %s" %
(title(i[0]), i[1][0])
for i in form.errors.items()])
messages.error(request, error)
return redirect("%s#resources" % reverse_lazy(
"dashboard.views.detail", kwargs={'pk': self.object.pk}))
def __shut_down(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
......@@ -763,8 +747,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(TemplateDetail, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
obj = self.get_object()
context = super(TemplateDetail, self).get_context_data(**kwargs)
context['acl'] = get_vm_acl_data(self.get_object())
context['acl'] = get_vm_acl_data(obj)
context['disks'] = obj.disks.all()
context['disk_add_form'] = DiskAddForm(
user=self.request.user,
is_template=True,
object_pk=obj.pk,
prefix="disk",
)
return context
def get_success_url(self):
......@@ -1759,3 +1751,44 @@ def circle_login(request):
}
return login(request, authentication_form=authentication_form,
extra_context=extra_context)
class DiskAddView(TemplateView):
def post(self, *args, **kwargs):
is_template = self.request.POST.get("disk-is_template")
object_pk = self.request.POST.get("disk-object_pk")
is_template = int(is_template) == 1
if is_template:
obj = InstanceTemplate.objects.get(pk=object_pk)
else:
obj = Instance.objects.get(pk=object_pk)
if not obj.has_level(self.request.user, 'owner'):
raise PermissionDenied()
form = DiskAddForm(
self.request.POST,
user=self.request.user,
is_template=is_template, object_pk=object_pk,
prefix="disk"
)
if form.is_valid():
if form.cleaned_data.get("size"):
messages.success(self.request, _("Disk successfully added!"))
else:
messages.success(self.request, _("Disk download started!"))
form.save()
else:
error = "<br /> ".join(["<strong>%s</strong>: %s" %
(title_filter(i[0]), i[1][0])
for i in form.errors.items()])
messages.error(self.request, error)
if is_template:
r = obj.get_absolute_url()
else:
r = obj.get_absolute_url()
r = "%s#resources" % r
return redirect(r)
......@@ -16,6 +16,7 @@ from datetime import timedelta
from acl.models import AclBase
from .tasks import local_tasks, remote_tasks
from celery.exceptions import TimeoutError
from manager.mancelery import celery
from common.models import ActivityModel, activitycontextimpl, WorkerNotFound
logger = logging.getLogger(__name__)
......@@ -134,6 +135,19 @@ class Disk(AclBase, TimeStampedModel):
'raw-rw': 'vd',
}[self.type]
def is_downloading(self):
da = DiskActivity.objects.filter(disk=self).latest("created")
return (da.activity_code == "storage.Disk.download"
and da.succeeded is None)
def get_download_percentage(self):
if not self.is_downloading():
return None
task = DiskActivity.objects.latest("created").task_uuid
result = celery.AsyncResult(id=task)
return result.info.get("percent")
def is_deletable(self):
"""Returns whether the file can be deleted.
......@@ -305,8 +319,8 @@ class Disk(AclBase, TimeStampedModel):
"""
kwargs.update({'cls': cls, 'url': url,
'instance': instance, 'user': user})
return local_tasks.create_from_url.apply_async(kwargs=kwargs,
queue='localhost.man')
return local_tasks.create_from_url.apply_async(
kwargs=kwargs, queue='localhost.man')
@classmethod
def create_from_url(cls, url, instance=None, user=None,
......
......@@ -42,18 +42,18 @@ def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=user)
class create_from_url(AbortableTask):
class CreateFromURLTask(AbortableTask):
def __init__(self):
self.bind(celery)
def run(self, **kwargs):
Disk = kwargs['cls']
url = kwargs['url']
params = kwargs['params']
user = kwargs['user']
Disk.create_from_url(url=url,
params=params,
Disk = kwargs.pop('cls')
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=create_from_url.request.id,
abortable_task=self,
user=user)
**kwargs)
create_from_url = CreateFromURLTask()
@celery.task
......
......@@ -152,6 +152,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
if is_new:
self.set_level(self.owner, 'owner')
@permalink
def get_absolute_url(self):
return ('dashboard.views.template-detail', None, {'pk': self.pk})
class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment