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
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")
......@@ -740,27 +741,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
......@@ -768,11 +803,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>
</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>
......
......@@ -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 />
......
......@@ -252,8 +252,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())
......@@ -263,8 +267,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, VmRenewView,
VmMigrateView, VmDetailVncTokenView, VmRenewView, DiskAddView,
)
urlpatterns = patterns(
......@@ -91,4 +91,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.template import RequestContext
......@@ -209,7 +209,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")
......@@ -228,7 +231,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,
......@@ -404,24 +406,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'):
......@@ -785,8 +769,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):
......@@ -1941,3 +1933,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)
# -*- 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
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__)
......@@ -65,7 +66,8 @@ class Disk(AclBase, TimeStampedModel):
TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
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"),
help_text=_("The datastore that holds the disk."))
type = CharField(max_length=10, choices=TYPES)
......@@ -112,7 +114,7 @@ class Disk(AclBase, TimeStampedModel):
return join(self.datastore.path, self.filename)
@property
def format(self):
def vm_format(self):
"""Returns the proper file format for different type of images."""
return {
'qcow2-norm': 'qcow2',
......@@ -123,6 +125,17 @@ class Disk(AclBase, TimeStampedModel):
}[self.type]
@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):
"""Returns the proper device prefix for different file format."""
return {
......@@ -133,6 +146,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.
......@@ -171,18 +197,17 @@ class Disk(AclBase, TimeStampedModel):
if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type)
filename = self.filename if self.type == 'iso' else None
new_type = type_mapping[self.type]
return Disk.objects.create(base=self, datastore=self.datastore,
filename=filename, name=self.name,
size=self.size, type=new_type)
return Disk.create(base=self, datastore=self.datastore,
name=self.name, size=self.size,
type=new_type)
def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver."""
return {
'source': self.path,
'driver_type': self.format,
'driver_type': self.vm_format,
'driver_cache': 'none',
'target_device': self.device_type + self.dev_num,
'disk_device': 'cdrom' if self.type == 'iso' else 'disk'
......@@ -196,7 +221,7 @@ class Disk(AclBase, TimeStampedModel):
'format': self.format,
'size': self.size,
'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):
......@@ -214,11 +239,6 @@ class Disk(AclBase, TimeStampedModel):
self.size = self.base.size
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):
"""Reify the disk model on the associated data store.
......@@ -249,7 +269,7 @@ class Disk(AclBase, TimeStampedModel):
# Delegate create / snapshot jobs
queue_name = self.get_remote_queue_name('storage')
disk_desc = self.get_disk_desc()
if self.type == 'qcow2-snap':
if self.base is not None:
with act.sub_activity('creating_snapshot'):
remote_tasks.snapshot.apply_async(args=[disk_desc],
queue=queue_name
......@@ -271,10 +291,12 @@ class Disk(AclBase, TimeStampedModel):
return local_tasks.deploy.apply_async(args=[self, user],
queue="localhost.man")
def generate_filename(self):
"""Generate a unique filename and set it on the object.
"""
self.filename = str(uuid.uuid4())
@classmethod
def create(cls, **params):
datastore = params.pop('datastore', DataStore.objects.get())
disk = cls(filename=str(uuid.uuid4()), datastore=datastore, **params)
disk.save()
return disk
@classmethod
def create_empty(cls, instance=None, user=None, **kwargs):
......@@ -287,13 +309,8 @@ class Disk(AclBase, TimeStampedModel):
:return: Disk object without a real image, to be .deploy()ed later.
"""
with disk_activity(code_suffix="create", user=user) as act:
disk = cls(**kwargs)
if disk.filename is None:
disk.generate_filename()
disk.save()
act.disk = disk
act.save()
disk = cls.create(**kwargs)
with disk_activity(code_suffix="create", user=user, disk=disk):
if instance:
instance.disks.add(disk)
return disk
......@@ -314,8 +331,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,
......@@ -335,13 +352,9 @@ class Disk(AclBase, TimeStampedModel):
:rtype: Disk
"""
kwargs.setdefault('name', url.split('/')[-1])
disk = cls(**kwargs)
disk.generate_filename()
disk.type = "iso"
disk.size = 1
disk = Disk.create(type="iso", size=1, **kwargs)
# TODO get proper datastore
disk.datastore = DataStore.objects.get()
disk.save()
if instance:
instance.disks.add(disk)
queue_name = disk.get_remote_queue_name('storage')
......@@ -403,7 +416,16 @@ class Disk(AclBase, TimeStampedModel):
local_tasks.restore.apply_async(args=[self, user],
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 = {
'qcow2-snap': ('qcow2-norm', self.base),
}
......@@ -416,25 +438,24 @@ class Disk(AclBase, TimeStampedModel):
# from this point on, the caller has to guarantee that the disk is not
# going to be used until the operation is complete
with disk_activity(code_suffix='save_as', disk=self,
task_uuid=task_uuid, user=user, timeout=300):
new_type, new_base = mapping[self.type]
new_type, new_base = mapping[self.type]
disk = Disk.objects.create(base=new_base, datastore=self.datastore,
name=self.name, size=self.size,
type=new_type)
disk = Disk.create(base=new_base, datastore=self.datastore,
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')
remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
disk.get_disk_desc()],
queue=queue_name
).get(timeout=timeout)
).get() # Timeout
disk.ready = True
disk.save()
return disk
return disk
class DiskActivity(ActivityModel):
......
......@@ -22,6 +22,12 @@ def check_queue(storage, queue_id):
@celery.task
def save_as(disk, timeout, user):