Commit 7fa48338 by Őry Máté

Merge branch 'feature-save-as'

 Requires support in circle/storagedriver (at least
storagedriver/94379fb).

 add InstanceTemplate.save_as/_async
 #24 create disk from iso url
 add Disk.create (replace all Disk.objects.create and
Disk.__init__ calls)
 add Disk.create_empty
parents 060ea3aa d1f3c6bb
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")
...@@ -740,27 +741,61 @@ class LeaseForm(forms.ModelForm): ...@@ -740,27 +741,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
...@@ -768,11 +803,23 @@ class DiskAddForm(forms.Form): ...@@ -768,11 +803,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 />
......
...@@ -252,8 +252,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -252,8 +252,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())
...@@ -263,8 +267,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -263,8 +267,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, VmRenewView, VmMigrateView, VmDetailVncTokenView, VmRenewView, DiskAddView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -91,4 +91,7 @@ urlpatterns = patterns( ...@@ -91,4 +91,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.template import RequestContext from django.template import RequestContext
...@@ -209,7 +209,10 @@ class VmDetailView(CheckedDetailView): ...@@ -209,7 +209,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")
...@@ -228,7 +231,6 @@ class VmDetailView(CheckedDetailView): ...@@ -228,7 +231,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,
...@@ -404,24 +406,6 @@ class VmDetailView(CheckedDetailView): ...@@ -404,24 +406,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'):
...@@ -785,8 +769,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -785,8 +769,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):
...@@ -1941,3 +1933,44 @@ def circle_login(request): ...@@ -1941,3 +1933,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)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'Disk', fields ['filename']
db.create_unique(u'storage_disk', ['filename'])
def backwards(self, orm):
# Removing unique constraint on 'Disk', fields ['filename']
db.delete_unique(u'storage_disk', ['filename'])
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'storage.diskactivity': {
'Meta': {'object_name': 'DiskActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'disk': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['storage.Disk']"}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['storage.DiskActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['storage']
\ No newline at end of file
...@@ -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__)
...@@ -65,7 +66,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -65,7 +66,8 @@ class Disk(AclBase, TimeStampedModel):
TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'), TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')] ('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
name = CharField(blank=True, max_length=100, verbose_name=_("name")) name = CharField(blank=True, max_length=100, verbose_name=_("name"))
filename = CharField(max_length=256, verbose_name=_("filename")) filename = CharField(max_length=256, unique=True,
verbose_name=_("filename"))
datastore = ForeignKey(DataStore, verbose_name=_("datastore"), datastore = ForeignKey(DataStore, verbose_name=_("datastore"),
help_text=_("The datastore that holds the disk.")) help_text=_("The datastore that holds the disk."))
type = CharField(max_length=10, choices=TYPES) type = CharField(max_length=10, choices=TYPES)
...@@ -112,7 +114,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -112,7 +114,7 @@ class Disk(AclBase, TimeStampedModel):
return join(self.datastore.path, self.filename) return join(self.datastore.path, self.filename)
@property @property
def format(self): def vm_format(self):
"""Returns the proper file format for different type of images.""" """Returns the proper file format for different type of images."""
return { return {
'qcow2-norm': 'qcow2', 'qcow2-norm': 'qcow2',
...@@ -123,6 +125,17 @@ class Disk(AclBase, TimeStampedModel): ...@@ -123,6 +125,17 @@ class Disk(AclBase, TimeStampedModel):
}[self.type] }[self.type]
@property @property
def format(self):
"""Returns the proper file format for different type of images."""
return {
'qcow2-norm': 'qcow2',
'qcow2-snap': 'qcow2',
'iso': 'iso',
'raw-ro': 'raw',
'raw-rw': 'raw',
}[self.type]
@property
def device_type(self): def device_type(self):
"""Returns the proper device prefix for different file format.""" """Returns the proper device prefix for different file format."""
return { return {
...@@ -133,6 +146,19 @@ class Disk(AclBase, TimeStampedModel): ...@@ -133,6 +146,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.
...@@ -171,18 +197,17 @@ class Disk(AclBase, TimeStampedModel): ...@@ -171,18 +197,17 @@ class Disk(AclBase, TimeStampedModel):
if self.type not in type_mapping.keys(): if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self.type)
filename = self.filename if self.type == 'iso' else None
new_type = type_mapping[self.type] new_type = type_mapping[self.type]
return Disk.objects.create(base=self, datastore=self.datastore, return Disk.create(base=self, datastore=self.datastore,
filename=filename, name=self.name, name=self.name, size=self.size,
size=self.size, type=new_type) type=new_type)
def get_vmdisk_desc(self): def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver.""" """Serialize disk object to the vmdriver."""
return { return {
'source': self.path, 'source': self.path,
'driver_type': self.format, 'driver_type': self.vm_format,
'driver_cache': 'none', 'driver_cache': 'none',
'target_device': self.device_type + self.dev_num, 'target_device': self.device_type + self.dev_num,
'disk_device': 'cdrom' if self.type == 'iso' else 'disk' 'disk_device': 'cdrom' if self.type == 'iso' else 'disk'
...@@ -196,7 +221,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -196,7 +221,7 @@ class Disk(AclBase, TimeStampedModel):
'format': self.format, 'format': self.format,
'size': self.size, 'size': self.size,
'base_name': self.base.filename if self.base else None, 'base_name': self.base.filename if self.base else None,
'type': 'snapshot' if self.type == 'qcow2-snap' else 'normal' 'type': 'snapshot' if self.base else 'normal'
} }
def get_remote_queue_name(self, queue_id='storage', check_worker=True): def get_remote_queue_name(self, queue_id='storage', check_worker=True):
...@@ -214,11 +239,6 @@ class Disk(AclBase, TimeStampedModel): ...@@ -214,11 +239,6 @@ class Disk(AclBase, TimeStampedModel):
self.size = self.base.size self.size = self.base.size
super(Disk, self).clean(*args, **kwargs) super(Disk, self).clean(*args, **kwargs)
def save(self, *args, **kwargs):
if self.filename is None:
self.generate_filename()
return super(Disk, self).save(*args, **kwargs)
def deploy(self, user=None, task_uuid=None, timeout=15): def deploy(self, user=None, task_uuid=None, timeout=15):
"""Reify the disk model on the associated data store. """Reify the disk model on the associated data store.
...@@ -249,7 +269,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -249,7 +269,7 @@ class Disk(AclBase, TimeStampedModel):
# Delegate create / snapshot jobs # Delegate create / snapshot jobs
queue_name = self.get_remote_queue_name('storage') queue_name = self.get_remote_queue_name('storage')
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.type == 'qcow2-snap': if self.base is not None:
with act.sub_activity('creating_snapshot'): with act.sub_activity('creating_snapshot'):
remote_tasks.snapshot.apply_async(args=[disk_desc], remote_tasks.snapshot.apply_async(args=[disk_desc],
queue=queue_name queue=queue_name
...@@ -271,10 +291,12 @@ class Disk(AclBase, TimeStampedModel): ...@@ -271,10 +291,12 @@ class Disk(AclBase, TimeStampedModel):
return local_tasks.deploy.apply_async(args=[self, user], return local_tasks.deploy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def generate_filename(self): @classmethod
"""Generate a unique filename and set it on the object. def create(cls, **params):
""" datastore = params.pop('datastore', DataStore.objects.get())
self.filename = str(uuid.uuid4()) disk = cls(filename=str(uuid.uuid4()), datastore=datastore, **params)
disk.save()
return disk
@classmethod @classmethod
def create_empty(cls, instance=None, user=None, **kwargs): def create_empty(cls, instance=None, user=None, **kwargs):
...@@ -287,13 +309,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -287,13 +309,8 @@ class Disk(AclBase, TimeStampedModel):
:return: Disk object without a real image, to be .deploy()ed later. :return: Disk object without a real image, to be .deploy()ed later.
""" """
with disk_activity(code_suffix="create", user=user) as act: disk = cls.create(**kwargs)
disk = cls(**kwargs) with disk_activity(code_suffix="create", user=user, disk=disk):
if disk.filename is None:
disk.generate_filename()
disk.save()
act.disk = disk
act.save()
if instance: if instance:
instance.disks.add(disk) instance.disks.add(disk)
return disk return disk
...@@ -314,8 +331,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -314,8 +331,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,
...@@ -335,13 +352,9 @@ class Disk(AclBase, TimeStampedModel): ...@@ -335,13 +352,9 @@ class Disk(AclBase, TimeStampedModel):
:rtype: Disk :rtype: Disk
""" """
kwargs.setdefault('name', url.split('/')[-1]) kwargs.setdefault('name', url.split('/')[-1])
disk = cls(**kwargs) disk = Disk.create(type="iso", size=1, **kwargs)
disk.generate_filename()
disk.type = "iso"
disk.size = 1
# TODO get proper datastore # TODO get proper datastore
disk.datastore = DataStore.objects.get() disk.datastore = DataStore.objects.get()
disk.save()
if instance: if instance:
instance.disks.add(disk) instance.disks.add(disk)
queue_name = disk.get_remote_queue_name('storage') queue_name = disk.get_remote_queue_name('storage')
...@@ -403,7 +416,16 @@ class Disk(AclBase, TimeStampedModel): ...@@ -403,7 +416,16 @@ class Disk(AclBase, TimeStampedModel):
local_tasks.restore.apply_async(args=[self, user], local_tasks.restore.apply_async(args=[self, user],
queue='localhost.man') queue='localhost.man')
def save_as(self, user=None, task_uuid=None, timeout=120): def save_as_async(self, disk, task_uuid=None, timeout=300, user=None):
return local_tasks.save_as.apply_async(args=[disk, timeout, user],
queue="localhost.man")
def save_as(self, user=None, task_uuid=None, timeout=300):
"""Save VM as template.
VM must be in STOPPED state to perform this action.
The timeout parameter is not used now.
"""
mapping = { mapping = {
'qcow2-snap': ('qcow2-norm', self.base), 'qcow2-snap': ('qcow2-norm', self.base),
} }
...@@ -416,25 +438,24 @@ class Disk(AclBase, TimeStampedModel): ...@@ -416,25 +438,24 @@ class Disk(AclBase, TimeStampedModel):
# from this point on, the caller has to guarantee that the disk is not # from this point on, the caller has to guarantee that the disk is not
# going to be used until the operation is complete # going to be used until the operation is complete
with disk_activity(code_suffix='save_as', disk=self, new_type, new_base = mapping[self.type]
task_uuid=task_uuid, user=user, timeout=300):
new_type, new_base = mapping[self.type] disk = Disk.create(base=new_base, datastore=self.datastore,
name=self.name, size=self.size,
disk = Disk.objects.create(base=new_base, datastore=self.datastore, type=new_type)
name=self.name, size=self.size,
type=new_type)
disk.save()
with disk_activity(code_suffix="save_as", disk=self,
user=user, task_uuid=None):
queue_name = self.get_remote_queue_name('storage') queue_name = self.get_remote_queue_name('storage')
remote_tasks.merge.apply_async(args=[self.get_disk_desc(), remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
disk.get_disk_desc()], disk.get_disk_desc()],
queue=queue_name queue=queue_name
).get(timeout=timeout) ).get() # Timeout
disk.ready = True disk.ready = True
disk.save() disk.save()
return disk return disk
class DiskActivity(ActivityModel): class DiskActivity(ActivityModel):
......
...@@ -22,6 +22,12 @@ def check_queue(storage, queue_id): ...@@ -22,6 +22,12 @@ def check_queue(storage, queue_id):
@celery.task @celery.task
def save_as(disk, timeout, user):
disk.save_disk_as(task_uuid=save_as.request.id, user=user,
disk=disk, timeout=timeout)
@celery.task
def deploy(disk, user): def deploy(disk, user):
disk.deploy(task_uuid=deploy.request.id, user=user) disk.deploy(task_uuid=deploy.request.id, user=user)
...@@ -36,18 +42,18 @@ def restore(disk, user): ...@@ -36,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):
...@@ -413,9 +417,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -413,9 +417,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
It is always on the first hard drive storage named cloud-<id>.dump It is always on the first hard drive storage named cloud-<id>.dump
""" """
datastore = self.disks.all()[0].datastore try:
path = datastore.path + '/' + self.vm_name + '.dump' datastore = self.disks.all()[0].datastore
return {'datastore': datastore, 'path': path} except:
return None
else:
path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path}
@property @property
def primary_host(self): def primary_host(self):
...@@ -803,9 +811,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -803,9 +811,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
:param act: Parent activity. :param act: Parent activity.
""" """
# Delete mem. dump if exists # Delete mem. dump if exists
queue_name = self.mem_dump['datastore'].get_remote_queue_name(
'storage')
try: try:
queue_name = self.mem_dump['datastore'].get_remote_queue_name(
'storage')
from storage.tasks.remote_tasks import delete_dump from storage.tasks.remote_tasks import delete_dump
delete_dump.apply_async(args=[self.mem_dump['path']], delete_dump.apply_async(args=[self.mem_dump['path']],
queue=queue_name).get(timeout=timeout) queue=queue_name).get(timeout=timeout)
...@@ -1079,47 +1087,54 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -1079,47 +1087,54 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
for net in self.interface_set.all(): for net in self.interface_set.all():
net.deploy() net.deploy()
def save_as_template(self, name, **kwargs): def save_as_template_async(self, name, user=None, **kwargs):
# prepare parameters return local_tasks.save_as_template.apply_async(
kwargs.setdefault('name', name) args=[self, name, user, kwargs], queue="localhost.man")
kwargs.setdefault('description', self.description)
kwargs.setdefault('parent', self.template)
kwargs.setdefault('num_cores', self.num_cores)
kwargs.setdefault('ram_size', self.ram_size)
kwargs.setdefault('max_ram_size', self.max_ram_size)
kwargs.setdefault('arch', self.arch)
kwargs.setdefault('priority', self.priority)
kwargs.setdefault('boot_menu', self.boot_menu)
kwargs.setdefault('raw_data', self.raw_data)
kwargs.setdefault('lease', self.lease)
kwargs.setdefault('access_method', self.access_method)
kwargs.setdefault('system', self.template.system
if self.template else None)
def __try_save_disk(disk):
try:
return disk.save_as() # can do in parallel
except Disk.WrongDiskTypeError:
return disk
# copy disks
disks = [__try_save_disk(disk) for disk in self.disks.all()]
kwargs.setdefault('disks', disks)
# create template and do additional setup
tmpl = InstanceTemplate(**kwargs)
# save template def save_as_template(self, name, task_uuid=None, user=None,
tmpl.save() timeout=300, **kwargs):
try: with instance_activity(code_suffix="save_as_template", instance=self,
# create interface templates task_uuid=task_uuid, user=user) as act:
for i in self.interface_set.all(): # prepare parameters
i.save_as_template(tmpl) kwargs.setdefault('name', name)
except: kwargs.setdefault('description', self.description)
tmpl.delete() kwargs.setdefault('parent', self.template)
raise kwargs.setdefault('num_cores', self.num_cores)
else: kwargs.setdefault('ram_size', self.ram_size)
return tmpl kwargs.setdefault('max_ram_size', self.max_ram_size)
kwargs.setdefault('arch', self.arch)
kwargs.setdefault('priority', self.priority)
kwargs.setdefault('boot_menu', self.boot_menu)
kwargs.setdefault('raw_data', self.raw_data)
kwargs.setdefault('lease', self.lease)
kwargs.setdefault('access_method', self.access_method)
kwargs.setdefault('system', self.template.system
if self.template else None)
def __try_save_disk(disk):
try:
return disk.save_as() # can do in parallel
except Disk.WrongDiskTypeError:
return disk
# create template and do additional setup
tmpl = InstanceTemplate(**kwargs)
tmpl.full_clean() # Avoiding database errors.
tmpl.save()
with act.sub_activity('saving_disks'):
tmpl.disks.add(*[__try_save_disk(disk)
for disk in self.disks.all()])
# save template
tmpl.save()
try:
# create interface templates
for i in self.interface_set.all():
i.save_as_template(tmpl)
except:
tmpl.delete()
raise
else:
return tmpl
def shutdown_and_save_as_template(self, name, user=None, task_uuid=None, def shutdown_and_save_as_template(self, name, user=None, task_uuid=None,
**kwargs): **kwargs):
......
...@@ -24,6 +24,12 @@ def destroy(instance, user): ...@@ -24,6 +24,12 @@ def destroy(instance, user):
@celery.task @celery.task
def save_as_template(instance, name, user, params):
instance.save_as_template(name, task_uuid=save_as_template.request.id,
user=user, **params)
@celery.task
def sleep(instance, user): def sleep(instance, user):
instance.sleep(task_uuid=sleep.request.id, user=user) instance.sleep(task_uuid=sleep.request.id, user=user)
......
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