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 from datetime import timedelta
import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
...@@ -20,7 +19,9 @@ from sizefield.widgets import FileSizeWidget ...@@ -20,7 +19,9 @@ from sizefield.widgets import FileSizeWidget
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk, DataStore 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() VLANS = Vlan.objects.all()
DISKS = Disk.objects.exclude(type="qcow2-snap") DISKS = Disk.objects.exclude(type="qcow2-snap")
...@@ -728,27 +729,61 @@ class LeaseForm(forms.ModelForm): ...@@ -728,27 +729,61 @@ class LeaseForm(forms.ModelForm):
class DiskAddForm(forms.Form): class DiskAddForm(forms.Form):
name = forms.CharField() 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): def clean_size(self):
size_in_bytes = self.cleaned_data.get("size") 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 " raise forms.ValidationError(_("Invalid format, you can use "
" GB or MB!")) " GB or MB!"))
return size_in_bytes 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 data = self.cleaned_data
d = Disk(
name=data['name'], if self.is_template:
filename=str(uuid.uuid4()), inst = InstanceTemplate.objects.get(pk=self.object_pk)
datastore=DataStore.objects.all()[0], else:
type="qcow2-norm", inst = Instance.objects.get(pk=self.object_pk)
size=data['size'],
dev_num="a", if data['size']:
) kwargs = {
d.save() 'name': data['name'],
vm.disks.add(d) '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 return d
@property @property
...@@ -756,11 +791,23 @@ class DiskAddForm(forms.Form): ...@@ -756,11 +791,23 @@ class DiskAddForm(forms.Form):
helper = FormHelper() helper = FormHelper()
helper.form_show_labels = False helper.form_show_labels = False
helper.layout = Layout( helper.layout = Layout(
Field("is_template", type="hidden"),
Field("object_pk", type="hidden"),
Field("name", placeholder=_("Name")), Field("name", placeholder=_("Name")),
Field("size", placeholder=_("Disk size (for example: 20GB, " Field("size", placeholder=_("Disk size (for example: 20GB, "
"1500MB)")), "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")) css_class="btn btn-success"))
return helper 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" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block content %} {% block content %}
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <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>
<div class="panel-body"> <div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %} <form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
...@@ -64,8 +65,35 @@ ...@@ -64,8 +65,35 @@
</form> </form>
</div> </div>
</div> </div>
</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><!-- .col-md-4 -->
</div><!-- .row -->
<style> <style>
......
...@@ -52,12 +52,15 @@ ...@@ -52,12 +52,15 @@
</a> </a>
</div> </div>
</h3> </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 %} {% for d in instance.disks.all %}
<h4 class="list-group-item-heading dashboard-vm-details-network-h3"> <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> </h4>
{% endfor %} {% endfor %}
</div> </div>
...@@ -67,7 +70,7 @@ ...@@ -67,7 +70,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div> <div>
<hr /> <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 %} {% crispy forms.disk_add_form %}
</form> </form>
<hr /> <hr />
......
...@@ -213,8 +213,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -213,8 +213,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
disks = inst.disks.count() disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a", response = c.post("/dashboard/disk/add/", {
'disk-size': 1}) 'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.assertEqual(disks, inst.disks.count()) self.assertEqual(disks, inst.disks.count())
...@@ -224,8 +228,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -224,8 +228,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
disks = inst.disks.count() disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a", response = c.post("/dashboard/disk/add/", {
'disk-size': 1}) 'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(disks + 1, inst.disks.count()) self.assertEqual(disks + 1, inst.disks.count())
......
...@@ -9,7 +9,7 @@ from .views import ( ...@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView, GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, VmDetailVncTokenView, VmMigrateView, DiskAddView, VmDetailVncTokenView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -89,4 +89,7 @@ urlpatterns = patterns( ...@@ -89,4 +89,7 @@ urlpatterns = patterns(
url(r'^notifications/$', NotificationView.as_view(), url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"), 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, ...@@ -22,7 +22,7 @@ from django.views.generic import (TemplateView, DetailView, View, DeleteView,
UpdateView, CreateView) UpdateView, CreateView)
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ 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.template.loader import render_to_string
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
...@@ -196,7 +196,10 @@ class VmDetailView(CheckedDetailView): ...@@ -196,7 +196,10 @@ class VmDetailView(CheckedDetailView):
).all() ).all()
context['acl'] = get_vm_acl_data(instance) context['acl'] = get_vm_acl_data(instance)
context['forms'] = { 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", context['os_type_icon'] = instance.os_type.replace("unknown",
"question") "question")
...@@ -215,7 +218,6 @@ class VmDetailView(CheckedDetailView): ...@@ -215,7 +218,6 @@ class VmDetailView(CheckedDetailView):
'port': self.__add_port, 'port': self.__add_port,
'new_network_vlan': self.__new_network, 'new_network_vlan': self.__new_network,
'save_as': self.__save_as, 'save_as': self.__save_as,
'disk-name': self.__add_disk,
'shut_down': self.__shut_down, 'shut_down': self.__shut_down,
'sleep': self.__sleep, 'sleep': self.__sleep,
'wake_up': self.__wake_up, 'wake_up': self.__wake_up,
...@@ -390,24 +392,6 @@ class VmDetailView(CheckedDetailView): ...@@ -390,24 +392,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.template-detail", return redirect(reverse_lazy("dashboard.views.template-detail",
kwargs={'pk': template.pk})) 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): def __shut_down(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, 'owner'):
...@@ -763,8 +747,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -763,8 +747,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(TemplateDetail, self).get(request, *args, **kwargs) return super(TemplateDetail, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
obj = self.get_object()
context = super(TemplateDetail, self).get_context_data(**kwargs) 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 return context
def get_success_url(self): def get_success_url(self):
...@@ -1759,3 +1751,44 @@ def circle_login(request): ...@@ -1759,3 +1751,44 @@ def circle_login(request):
} }
return login(request, authentication_form=authentication_form, return login(request, authentication_form=authentication_form,
extra_context=extra_context) 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 ...@@ -16,6 +16,7 @@ from datetime import timedelta
from acl.models import AclBase from acl.models import AclBase
from .tasks import local_tasks, remote_tasks from .tasks import local_tasks, remote_tasks
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from manager.mancelery import celery
from common.models import ActivityModel, activitycontextimpl, WorkerNotFound from common.models import ActivityModel, activitycontextimpl, WorkerNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -134,6 +135,19 @@ class Disk(AclBase, TimeStampedModel): ...@@ -134,6 +135,19 @@ class Disk(AclBase, TimeStampedModel):
'raw-rw': 'vd', 'raw-rw': 'vd',
}[self.type] }[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): def is_deletable(self):
"""Returns whether the file can be deleted. """Returns whether the file can be deleted.
...@@ -305,8 +319,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -305,8 +319,8 @@ class Disk(AclBase, TimeStampedModel):
""" """
kwargs.update({'cls': cls, 'url': url, kwargs.update({'cls': cls, 'url': url,
'instance': instance, 'user': user}) 'instance': instance, 'user': user})
return local_tasks.create_from_url.apply_async(kwargs=kwargs, return local_tasks.create_from_url.apply_async(
queue='localhost.man') kwargs=kwargs, queue='localhost.man')
@classmethod @classmethod
def create_from_url(cls, url, instance=None, user=None, def create_from_url(cls, url, instance=None, user=None,
......
...@@ -42,18 +42,18 @@ def restore(disk, user): ...@@ -42,18 +42,18 @@ def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=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): def run(self, **kwargs):
Disk = kwargs['cls'] Disk = kwargs.pop('cls')
url = kwargs['url'] Disk.create_from_url(url=kwargs.pop('url'),
params = kwargs['params']
user = kwargs['user']
Disk.create_from_url(url=url,
params=params,
task_uuid=create_from_url.request.id, task_uuid=create_from_url.request.id,
abortable_task=self, abortable_task=self,
user=user) **kwargs)
create_from_url = CreateFromURLTask()
@celery.task @celery.task
......
...@@ -152,6 +152,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -152,6 +152,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
if is_new: if is_new:
self.set_level(self.owner, 'owner') 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): 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