Commit 3b66d6d6 by Guba Sándor

Merge branch 'feature-base_template'

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 4d5d7287 f4067fef
...@@ -42,6 +42,23 @@ ...@@ -42,6 +42,23 @@
} }
}, },
{ {
"pk": 1,
"model": "storage.diskactivity",
"fields":{
"activity_code": "storage.Disk.create",
"succeeded": true,
"parent": null,
"created": "2014-03-18T15:44:37.671Z",
"started": "2014-03-18T15:44:37.671Z",
"finished": "2014-03-18T15:44:37.677Z",
"modified": "2014-03-18T15:44:37.679Z",
"task_uuid": null,
"user": 1,
"disk": 1,
"result":null
}
},
{
"pk": 1, "pk": 1,
"model": "auth.permission", "model": "auth.permission",
"fields": { "fields": {
...@@ -1497,7 +1514,7 @@ ...@@ -1497,7 +1514,7 @@
"boot_menu": false, "boot_menu": false,
"ram_size": 1024, "ram_size": 1024,
"modified": "2014-01-24T00:58:19.654Z", "modified": "2014-01-24T00:58:19.654Z",
"system": "", "system": "bubuntu",
"priority": 20, "priority": 20,
"access_method": "ssh", "access_method": "ssh",
"raw_data": "", "raw_data": "",
......
...@@ -445,15 +445,12 @@ class NodeForm(forms.ModelForm): ...@@ -445,15 +445,12 @@ class NodeForm(forms.ModelForm):
class TemplateForm(forms.ModelForm): class TemplateForm(forms.ModelForm):
networks = forms.ModelMultipleChoiceField( networks = forms.ModelMultipleChoiceField(
queryset=VLANS, required=False) queryset=VLANS, required=False)
system = forms.CharField(widget=forms.TextInput)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
parent = kwargs.pop("parent", None) parent = kwargs.pop("parent", None)
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
super(TemplateForm, self).__init__(*args, **kwargs) super(TemplateForm, self).__init__(*args, **kwargs)
self.fields['disks'] = forms.ModelMultipleChoiceField(
queryset=Disk.get_objects_with_level(
'user', self.user).exclude(type="qcow2-snap")
)
data = self.data.copy() data = self.data.copy()
data['owner'] = self.user.pk data['owner'] = self.user.pk
...@@ -468,7 +465,6 @@ class TemplateForm(forms.ModelForm): ...@@ -468,7 +465,6 @@ class TemplateForm(forms.ModelForm):
for f in fields: for f in fields:
self.initial[f] = parent[f] self.initial[f] = parent[f]
self.initial['lease'] = parent['lease_id'] self.initial['lease'] = parent['lease_id']
self.initial['disks'] = template.disks.all()
self.initial['parent'] = template self.initial['parent'] = template
self.initial['name'] = "Clone of %s" % self.initial['name'] self.initial['name'] = "Clone of %s" % self.initial['name']
self.for_networks = template self.for_networks = template
...@@ -506,8 +502,6 @@ class TemplateForm(forms.ModelForm): ...@@ -506,8 +502,6 @@ class TemplateForm(forms.ModelForm):
if commit: if commit:
instance.save() instance.save()
self.instance.disks = data['disks'] # TODO why do I need this
# create and/or delete InterfaceTemplates # create and/or delete InterfaceTemplates
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True) template=self.instance).values_list("vlan", flat=True)
...@@ -526,6 +520,7 @@ class TemplateForm(forms.ModelForm): ...@@ -526,6 +520,7 @@ class TemplateForm(forms.ModelForm):
kwargs_raw_data = {} kwargs_raw_data = {}
if not self.user.is_superuser: if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -585,7 +580,6 @@ class TemplateForm(forms.ModelForm): ...@@ -585,7 +580,6 @@ class TemplateForm(forms.ModelForm):
), ),
Fieldset( Fieldset(
_("External"), _("External"),
Field("disks"),
Field("networks"), Field("networks"),
Field("lease"), Field("lease"),
Field("tags"), Field("tags"),
...@@ -596,7 +590,7 @@ class TemplateForm(forms.ModelForm): ...@@ -596,7 +590,7 @@ class TemplateForm(forms.ModelForm):
class Meta: class Meta:
model = InstanceTemplate model = InstanceTemplate
exclude = ('state', ) exclude = ('state', 'disks', )
class LeaseForm(forms.ModelForm): class LeaseForm(forms.ModelForm):
......
...@@ -409,4 +409,11 @@ footer { ...@@ -409,4 +409,11 @@ footer {
footer a, footer a:hover, footer a:visited { footer a, footer a:hover, footer a:visited {
color: white; color: white;
text-decoration: underline; text-decoration: underline;
.template-disk-list {
list-style: none;
padding-left: 0;
}
.template-disk-list li {
padding-bottom: 5px;
} }
...@@ -122,6 +122,18 @@ $(function () { ...@@ -122,6 +122,18 @@ $(function () {
'redirect': dir}); 'redirect': dir});
return false; return false;
}); });
/* for disk remove buttons */
$('.disk-remove').click(function() {
var disk_pk = $(this).data('disk-pk');
addModalConfirmation(deleteObject,
{ 'url': '/dashboard/disk/' + disk_pk + '/remove/',
'data': [],
'pk': disk_pk,
'type': "disk",
});
return false;
});
/* for Node removes buttons */ /* for Node removes buttons */
$('.node-delete').click(function() { $('.node-delete').click(function() {
...@@ -273,9 +285,15 @@ function deleteObject(data) { ...@@ -273,9 +285,15 @@ function deleteObject(data) {
if(!data['redirect']) { if(!data['redirect']) {
selected = []; selected = [];
addMessage(re['message'], 'success'); addMessage(re['message'], 'success');
$('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() { if(data.type === "disk") {
$(this).remove(); // no need to remove them from DOM
}); $('a[data-disk-pk="' + data.pk + '"]').parent("li").fadeOut();
$('a[data-disk-pk="' + data.pk + '"]').parent("h4").fadeOut();
} else {
$('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
}
} else { } else {
window.location.replace('/dashboard'); window.location.replace('/dashboard');
} }
......
$(function() {
$(".disk-list-disk-percentage").each(function() {
var disk = $(this).data("disk-pk");
var element = $(this);
refreshDisk(disk, element);
});
});
function refreshDisk(disk, element) {
$.get("/dashboard/disk/" + disk + "/status/", function(result) {
if(result.percentage == null || result.failed == "True") {
location.reload();
} else {
var diff = result.percentage - parseInt(element.html());
var refresh = 5 - diff;
refresh = refresh < 1 ? 1 : (result.percentage == 0 ? 1 : refresh);
if(isNaN(refresh)) refresh = 2; // this should not happen
element.html(result.percentage);
setTimeout(function() {refreshDisk(disk, element)}, refresh * 1000);
}
});
}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<i class="{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% endif %}"></i> <i class="{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% if d.failed %}" style="color: #d9534f;{% endif %}{% endif %}"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.name }} (#{{ d.id }}) -
{% if not d.is_downloading %} {% if not d.is_downloading %}
{% if d.ready %} {% if not d.failed %}
{{ d.size|filesize }} {% if d.size %}{{ d.size|filesize }}{% endif %}
{% else %} {% else %}
<div class="label label-danger">failed</div> <div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>failed</div>
{% endif %} {% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} {% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}"
<div class="btn btn-xs btn-danger pull-right"><i class="icon-remove"></i> Remove</div> data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove">
<i class="icon-remove"></i>{% if long_remove %} Remove{% endif %}
</a>
<div style="clear: both;"></div>
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
<li> <li>
<i class="icon-file"></i> {% trans "Disks" %} <i class="icon-file"></i> {% trans "Disks" %}
<span style="float: right; text-align: right;"> <span style="float: right; text-align: right;">
{% for d in t.disks.all %}{{ d.name }} ({{ d.size|filesize }}){% if not forloop.last %}, {% endif %}{% endfor %} {% for d in t.disks.all %}{{ d.name }} ({% if d.size %}{{ d.size|filesize }}{% endif %}){% if not forloop.last %}, {% endif %}{% endfor %}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</li> </li>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
{% if text %} {% if text %}
{{ text }} {{ text|safe }}
{% else %} {% else %}
{%blocktrans with object=object%} {%blocktrans with object=object%}
Are you sure you want to delete <strong>{{ object }}</strong>? Are you sure you want to delete <strong>{{ object }}</strong>?
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% if text %} {% if text %}
{{ text }} {{ text|safe }}
{% else %} {% else %}
{%blocktrans with object=object%} {%blocktrans with object=object%}
Are you sure you want to delete <strong>{{ object }}</strong>? Are you sure you want to delete <strong>{{ object }}</strong>?
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
{% csrf_token %} {% csrf_token %}
<a class="btn btn-default">Back</a> <a class="btn btn-default">Back</a>
<input type="hidden" name="next" value="{{ request.GET.next }}"/> <input type="hidden" name="next" value="{{ request.GET.next }}"/>
<button class="btn btn-danger">Yes, delete</button> <button class="btn btn-danger">Yes</button>
</form> </form>
</div> </div>
</div> </div>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Create template" %}</h3> <h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Create base VM" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% with form=form %} {% with form=form %}
......
...@@ -72,7 +72,10 @@ ...@@ -72,7 +72,10 @@
<h4 class="no-margin"><i class="icon-file"></i> {% trans "Disk list" %}</h4> <h4 class="no-margin"><i class="icon-file"></i> {% trans "Disk list" %}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ul style="list-style: none; padding-left: 0;"> <ul class="template-disk-list">
{% if not disks %}
{% trans "No disks are added!" %}
{% endif %}
{% for d in disks %} {% for d in disks %}
<li> <li>
{% include "dashboard/_disk-list-element.html" %} {% include "dashboard/_disk-list-element.html" %}
...@@ -104,10 +107,14 @@ ...@@ -104,10 +107,14 @@
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script> {% endblock %}
$(function() {
$("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide(); {% block extra_js %}
}); <script>
</script> $(function() {
$("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide();
});
</script>
<script src="{{ STATIC_URL }}dashboard/disk-list.js"></script>
{% endblock %} {% endblock %}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a href="{% url "dashboard.views.template-create" %}" class="pull-right btn btn-success btn-xs"> <a href="{% url "dashboard.views.template-create" %}" class="pull-right btn btn-success btn-xs">
<i class="icon-plus"></i> new template <i class="icon-plus"></i> {% trans "new base vm" %}
</a> </a>
<h3 class="no-margin"><i class="icon-puzzle-piece"></i> {% trans "Templates" %}</h3> <h3 class="no-margin"><i class="icon-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div> </div>
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a href="{% url "dashboard.views.lease-create" %}" class="pull-right btn btn-success btn-xs" style="margin-right: 10px;"> <a href="{% url "dashboard.views.lease-create" %}" class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="icon-plus"></i> new lease <i class="icon-plus"></i> {% trans "new lease" %}
</a> </a>
<h3 class="no-margin"><i class="icon-time"></i> {% trans "Leases" %}</h3> <h3 class="no-margin"><i class="icon-time"></i> {% trans "Leases" %}</h3>
</div> </div>
......
...@@ -205,4 +205,5 @@ ...@@ -205,4 +205,5 @@
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
<script src="{{ STATIC_URL }}dashboard/disk-list.js"></script>
{% endblock %} {% endblock %}
...@@ -60,7 +60,9 @@ ...@@ -60,7 +60,9 @@
{% endif %} {% 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">
{% include "dashboard/_disk-list-element.html" %} {% with long_remove=True %}
{% include "dashboard/_disk-list-element.html" %}
{% endwith %}
</h4> </h4>
{% endfor %} {% endfor %}
</div> </div>
......
...@@ -187,6 +187,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -187,6 +187,7 @@ class VmDetailTest(LoginMixin, TestCase):
Vlan.objects.get(id=1).set_level(self.u1, 'user') Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/', response = c.post('/dashboard/vm/create/',
{'template': 1, {'template': 1,
'system': "bubi",
'cpu_priority': 1, 'cpu_count': 1, 'cpu_priority': 1, 'cpu_count': 1,
'ram_size': 1000}) 'ram_size': 1000})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
...@@ -199,6 +200,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -199,6 +200,7 @@ class VmDetailTest(LoginMixin, TestCase):
Vlan.objects.get(id=1).set_level(self.u1, 'user') Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/', response = c.post('/dashboard/vm/create/',
{'template': 1, {'template': 1,
'system': "bubi",
'cpu_priority': 1, 'cpu_count': 1, 'cpu_priority': 1, 'cpu_count': 1,
'ram_size': 1000}) 'ram_size': 1000})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -208,6 +210,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -208,6 +210,7 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
response = c.post('/dashboard/vm/create/', response = c.post('/dashboard/vm/create/',
{'template': 1, {'template': 1,
'system': "bubi",
'cpu_priority': 1, 'cpu_count': 1, 'cpu_priority': 1, 'cpu_count': 1,
'ram_size': 1000}) 'ram_size': 1000})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
......
...@@ -10,7 +10,8 @@ from .views import ( ...@@ -10,7 +10,8 @@ from .views import (
TemplateCreate, TemplateDelete, TemplateDetail, TemplateList, TemplateCreate, TemplateDelete, TemplateDetail, TemplateList,
TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate, TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate,
VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList, VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList,
VmMassDelete, VmMigrateView, VmRenewView, VmMassDelete, VmMigrateView, VmRenewView, DiskRemoveView,
get_disk_download_status,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -99,6 +100,11 @@ urlpatterns = patterns( ...@@ -99,6 +100,11 @@ urlpatterns = patterns(
url(r'^disk/add/$', DiskAddView.as_view(), url(r'^disk/add/$', DiskAddView.as_view(),
name="dashboard.views.disk-add"), name="dashboard.views.disk-add"),
url(r'^disk/(?P<pk>\d+)/remove/$', DiskRemoveView.as_view(),
name="dashboard.views.disk-remove"),
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"),
url(r'^profile/$', MyPreferencesView.as_view(), url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile"), name="dashboard.views.profile"),
) )
from __future__ import unicode_literals
from os import getenv from os import getenv
import json import json
import logging import logging
...@@ -43,6 +45,7 @@ from vm.models import ( ...@@ -43,6 +45,7 @@ from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait, InterfaceTemplate, Lease, Node, NodeActivity, Trait,
) )
from storage.models import Disk
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite, Profile from dashboard.models import Favourite, Profile
...@@ -275,6 +278,7 @@ class VmDetailView(CheckedDetailView): ...@@ -275,6 +278,7 @@ class VmDetailView(CheckedDetailView):
resources = { resources = {
'num_cores': request.POST.get('cpu-count'), 'num_cores': request.POST.get('cpu-count'),
'ram_size': request.POST.get('ram-size'), 'ram_size': request.POST.get('ram-size'),
'max_ram_size': request.POST.get('ram-size'), # TODO: max_ram
'priority': request.POST.get('cpu-priority') 'priority': request.POST.get('cpu-priority')
} }
Instance.objects.filter(pk=self.object.pk).update(**resources) Instance.objects.filter(pk=self.object.pk).update(**resources)
...@@ -419,12 +423,10 @@ class VmDetailView(CheckedDetailView): ...@@ -419,12 +423,10 @@ class VmDetailView(CheckedDetailView):
new_name = "Saved from %s (#%d) at %s" % ( new_name = "Saved from %s (#%d) at %s" % (
self.object.name, self.object.pk, date self.object.name, self.object.pk, date
) )
template = self.object.save_as_template(name=new_name, self.object.save_as_template_async(name=new_name,
owner=request.user) user=request.user)
messages.success(request, _("Instance successfully saved as template, " messages.success(request, _("Saving instance as template!"))
"please rename it!")) return redirect("%s#activity" % self.object.get_absolute_url())
return redirect(reverse_lazy("dashboard.views.template-detail",
kwargs={'pk': template.pk}))
def __shut_down(self, request): def __shut_down(self, request):
self.object = self.get_object() self.object = self.get_object()
...@@ -765,13 +767,29 @@ class TemplateCreate(SuccessMessageMixin, CreateView): ...@@ -765,13 +767,29 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
form = self.form_class(request.POST, user=request.user) form = self.form_class(request.POST, user=request.user)
if not form.is_valid(): if not form.is_valid():
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
post = form.cleaned_data else:
for disk in post['disks']: post = form.cleaned_data
if not disk.has_level(request.user, 'user'):
raise PermissionDenied() networks = self.__create_networks(post.pop("networks"))
req_traits = post.pop("req_traits")
tags = post.pop("tags")
post['pw'] = User.objects.make_random_password()
post.pop("parent")
post['max_ram_size'] = post['ram_size']
inst = Instance.create(params=post, disks=[], networks=networks,
tags=tags, req_traits=req_traits)
messages.success(request, _("Your disk has been created, "
"you can now add disks to it!"))
return redirect("%s#resources" % inst.get_absolute_url())
return super(TemplateCreate, self).post(self, request, args, kwargs) return super(TemplateCreate, self).post(self, request, args, kwargs)
def __create_networks(self, vlans):
networks = []
for v in vlans:
networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
return networks
def get_success_url(self): def get_success_url(self):
return reverse_lazy("dashboard.views.template-list") return reverse_lazy("dashboard.views.template-list")
...@@ -2115,3 +2133,64 @@ def set_language_cookie(request, response, lang=None): ...@@ -2115,3 +2133,64 @@ def set_language_cookie(request, response, lang=None):
cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language') cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language')
response.set_cookie(cname, lang, 365 * 86400) response.set_cookie(cname, lang, 365 * 86400)
class DiskRemoveView(DeleteView):
model = Disk
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(DiskRemoveView, self).get_context_data(**kwargs)
disk = self.get_object()
app = disk.get_appliance()
context['title'] = _("Disk remove confirmation")
context['text'] = _("Are you sure you want to remove "
"<strong>%(disk)s</strong> from "
"<strong>%(app)s</strong>?" % {'disk': disk,
'app': app}
)
return context
def delete(self, request, *args, **kwargs):
disk = self.get_object()
if not disk.has_level(request.user, 'owner'):
raise PermissionDenied()
disk = self.get_object()
app = disk.get_appliance()
app.disks.remove(disk)
disk.destroy()
next_url = request.POST.get("next")
success_url = next_url if next_url else app.get_absolute_url()
success_message = _("Disk successfully removed!")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#resources" % success_url)
@require_GET
def get_disk_download_status(request, pk):
disk = Disk.objects.get(pk=pk)
if not disk.has_level(request.user, 'owner'):
raise PermissionDenied()
return HttpResponse(
json.dumps({
'percentage': disk.get_download_percentage(),
'failed': disk.failed
}),
content_type="application/json",
)
# coding=utf-8 # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from contextlib import contextmanager from contextlib import contextmanager
import logging import logging
...@@ -107,10 +108,35 @@ class Disk(AclBase, TimeStampedModel): ...@@ -107,10 +108,35 @@ class Disk(AclBase, TimeStampedModel):
self.disk = disk self.disk = disk
class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk.
"""
def __init__(self, disk, message=None):
if message is None:
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it has never been"
"deployed." % (disk.name, disk.filename))
Exception.__init__(self, message)
self.disk = disk
@property @property
def ready(self): def ready(self):
""" Returns True if the disk is physically ready on the storage.
It needs at least 1 successfull deploy action.
"""
return self.activity_log.filter(activity_code__endswith="deploy", return self.activity_log.filter(activity_code__endswith="deploy",
succeeded__isnull=False) succeeded=True)
@property
def failed(self):
""" Returns True if the last activity on the disk is failed.
"""
result = self.activity_log.all().order_by('-id')[0].succeeded
return not (result is None) and not result
@property @property
def path(self): def path(self):
...@@ -155,18 +181,22 @@ class Disk(AclBase, TimeStampedModel): ...@@ -155,18 +181,22 @@ class Disk(AclBase, TimeStampedModel):
}[self.type] }[self.type]
def is_downloading(self): def is_downloading(self):
return self.activity_log.filter( return self.size is None and not self.failed
activity_code__endswith="downloading_disk",
succeeded__isnull=True)
def get_download_percentage(self): def get_download_percentage(self):
if not self.is_downloading(): if not self.is_downloading():
return None return None
task = self.activity_log.filter( try:
activity_code__endswith="deploy", task = self.activity_log.filter(
succeeded__isnull=True)[0].task_uuid activity_code__endswith="deploy",
result = celery.AsyncResult(id=task) succeeded__isnull=True)[0].task_uuid
return result.info.get("percent") result = celery.AsyncResult(id=task)
return result.info.get("percent")
except:
return 0
def get_latest_activity_result(self):
return self.activity_log.latest("pk").result
@property @property
def is_deletable(self): def is_deletable(self):
...@@ -192,6 +222,17 @@ class Disk(AclBase, TimeStampedModel): ...@@ -192,6 +222,17 @@ class Disk(AclBase, TimeStampedModel):
""" """
return any(i.state != 'STOPPED' for i in self.instance_set.all()) return any(i.state != 'STOPPED' for i in self.instance_set.all())
def get_appliance(self):
"""Return an Instance or InstanceTemplate object where the disk is used
"""
instance = self.instance_set.all()
template = self.template_set.all()
app = list(instance) + list(template)
if len(app) > 0:
return app[0]
else:
return None
def get_exclusive(self): def get_exclusive(self):
"""Get an instance of the disk for exclusive usage. """Get an instance of the disk for exclusive usage.
...@@ -247,7 +288,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -247,7 +288,7 @@ class Disk(AclBase, TimeStampedModel):
return u"%s (#%d)" % (self.name, self.id or 0) return u"%s (#%d)" % (self.name, self.id or 0)
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
if self.size == "" and self.base: if (self.size is None or "") and self.base:
self.size = self.base.size self.size = self.base.size
super(Disk, self).clean(*args, **kwargs) super(Disk, self).clean(*args, **kwargs)
...@@ -305,7 +346,9 @@ class Disk(AclBase, TimeStampedModel): ...@@ -305,7 +346,9 @@ class Disk(AclBase, TimeStampedModel):
""" """
datastore = params.pop('datastore', DataStore.objects.get()) datastore = params.pop('datastore', DataStore.objects.get())
disk = cls(filename=str(uuid.uuid4()), datastore=datastore, **params) disk = cls(filename=str(uuid.uuid4()), datastore=datastore, **params)
disk.clean()
disk.save() disk.save()
logger.debug("Disk created: %s", params)
with disk_activity(code_suffix="create", with disk_activity(code_suffix="create",
user=user, user=user,
disk=disk): disk=disk):
...@@ -366,8 +409,6 @@ class Disk(AclBase, TimeStampedModel): ...@@ -366,8 +409,6 @@ class Disk(AclBase, TimeStampedModel):
kwargs.setdefault('name', url.split('/')[-1]) kwargs.setdefault('name', url.split('/')[-1])
disk = Disk.create(type="iso", instance=instance, user=user, disk = Disk.create(type="iso", instance=instance, user=user,
size=None, **kwargs) size=None, **kwargs)
# TODO get proper datastore
disk.datastore = DataStore.objects.get()
queue_name = disk.get_remote_queue_name('storage') queue_name = disk.get_remote_queue_name('storage')
def __on_abort(activity, error): def __on_abort(activity, error):
...@@ -439,6 +480,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -439,6 +480,7 @@ class Disk(AclBase, TimeStampedModel):
""" """
mapping = { mapping = {
'qcow2-snap': ('qcow2-norm', self.base), 'qcow2-snap': ('qcow2-norm', self.base),
'qcow2-norm': ('qcow2-norm', self),
} }
if self.type not in mapping.keys(): if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self.type)
...@@ -446,6 +488,9 @@ class Disk(AclBase, TimeStampedModel): ...@@ -446,6 +488,9 @@ class Disk(AclBase, TimeStampedModel):
if self.is_in_use: if self.is_in_use:
raise self.DiskInUseError(self) raise self.DiskInUseError(self)
if not self.ready:
raise self.DiskIsNotReady(self)
# 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
...@@ -455,7 +500,6 @@ class Disk(AclBase, TimeStampedModel): ...@@ -455,7 +500,6 @@ class Disk(AclBase, TimeStampedModel):
name=self.name, size=self.size, name=self.name, size=self.size,
type=new_type) type=new_type)
disk.save()
with disk_activity(code_suffix="save_as", disk=self, with disk_activity(code_suffix="save_as", disk=self,
user=user, task_uuid=task_uuid): user=user, task_uuid=task_uuid):
with disk_activity(code_suffix="deploy", disk=disk, with disk_activity(code_suffix="deploy", disk=disk,
......
from storage.models import DataStore from storage.models import DataStore
import os
from manager.mancelery import celery from manager.mancelery import celery
import logging import logging
from storage.tasks import remote_tasks from storage.tasks import remote_tasks
...@@ -16,13 +15,15 @@ def garbage_collector(timeout=15): ...@@ -16,13 +15,15 @@ def garbage_collector(timeout=15):
deletes oldest images from trash. deletes oldest images from trash.
:param timeout: Seconds before TimeOut exception :param timeout: Seconds before TimeOut exception
:type timeoit: int :type timeout: int
""" """
for ds in DataStore.objects.all(): for ds in DataStore.objects.all():
file_list = os.listdir(ds.path)
disk_list = ds.get_deletable_disks()
queue_name = ds.get_remote_queue_name('storage') queue_name = ds.get_remote_queue_name('storage')
for i in set(file_list).intersection(disk_list): files = set(remote_tasks.list_files.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set(ds.get_deletable_disks())
queue_name = ds.get_remote_queue_name('storage')
for i in disks & files:
logger.info("Image: %s at Datastore: %s moved to trash folder." % logger.info("Image: %s at Datastore: %s moved to trash folder." %
(i, ds.path)) (i, ds.path))
remote_tasks.move_to_trash.apply_async( remote_tasks.move_to_trash.apply_async(
......
...@@ -2,9 +2,11 @@ from datetime import timedelta ...@@ -2,9 +2,11 @@ from datetime import timedelta
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from mock import MagicMock, Mock
from ..models import Disk, DataStore from ..models import Disk, DataStore
old = timezone.now() - timedelta(days=2) old = timezone.now() - timedelta(days=2)
new = timezone.now() - timedelta(hours=2) new = timezone.now() - timedelta(hours=2)
...@@ -46,3 +48,41 @@ class DiskTestCase(TestCase): ...@@ -46,3 +48,41 @@ class DiskTestCase(TestCase):
self._disk(base=d, destroyed=new) self._disk(base=d, destroyed=new)
self._disk(base=d) self._disk(base=d)
assert not d.is_deletable assert not d.is_deletable
def test_save_as_disk_in_use_error(self):
class MockException(Exception):
pass
d = MagicMock(spec=Disk)
d.DiskInUseError = MockException
d.type = "qcow2-norm"
d.is_in_use = True
with self.assertRaises(MockException):
Disk.save_as(d)
def test_save_as_wrong_type(self):
class MockException(Exception):
pass
d = MagicMock(spec=Disk)
d.WrongDiskTypeError = MockException
d.type = "wrong"
with self.assertRaises(MockException):
Disk.save_as(d)
def test_save_as_disk_not_ready(self):
class MockException(Exception):
pass
d = MagicMock(spec=Disk)
d.DiskIsNotReady = MockException
d.type = "qcow2-norm"
d.is_in_use = False
d.ready = False
with self.assertRaises(MockException):
Disk.save_as(d)
def test_download_percentage_no_download(self):
d = MagicMock(spec=Disk)
d.is_downloading = Mock(return_value=False)
assert Disk.get_download_percentage(d) is None
...@@ -93,7 +93,6 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -93,7 +93,6 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
"for hosting the VM."), "for hosting the VM."),
verbose_name=_("required traits")) verbose_name=_("required traits"))
system = TextField(verbose_name=_('operating system'), system = TextField(verbose_name=_('operating system'),
blank=True,
help_text=(_('Name of operating system in ' help_text=(_('Name of operating system in '
'format like "%s".') % 'format like "%s".') %
'Ubuntu 12.04 LTS Desktop amd64')) 'Ubuntu 12.04 LTS Desktop amd64'))
...@@ -250,7 +249,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -250,7 +249,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
if message is None: if message is None:
message = ("The instance's current state (%s) is " message = ("The instance's current state (%s) is "
"inappropriate for the invoked operation." "inappropriate for the invoked operation."
% instance.state) % instance.status)
Exception.__init__(self, message) Exception.__init__(self, message)
...@@ -266,7 +265,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -266,7 +265,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
@property @property
def is_running(self): def is_running(self):
return self.state == 'RUNNING' """Check if VM is in running state.
"""
return self.status == 'RUNNING'
@property @property
def state(self): def state(self):
...@@ -307,6 +308,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -307,6 +308,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
@classmethod @classmethod
def create(cls, params, disks, networks, req_traits, tags): def create(cls, params, disks, networks, req_traits, tags):
""" Create new Instance object.
"""
# create instance and do additional setup # create instance and do additional setup
inst = cls(**params) inst = cls(**params)
...@@ -379,7 +382,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -379,7 +382,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
# prepare parameters # prepare parameters
common_fields = ['name', 'description', 'num_cores', 'ram_size', common_fields = ['name', 'description', 'num_cores', 'ram_size',
'max_ram_size', 'arch', 'priority', 'boot_menu', 'max_ram_size', 'arch', 'priority', 'boot_menu',
'raw_data', 'lease', 'access_method'] 'raw_data', 'lease', 'access_method', 'system']
params = dict(template=template, owner=owner, pw=pwgen()) params = dict(template=template, owner=owner, pw=pwgen())
params.update([(f, getattr(template, f)) for f in common_fields]) params.update([(f, getattr(template, f)) for f in common_fields])
params.update(kwargs) # override defaults w/ user supplied values params.update(kwargs) # override defaults w/ user supplied values
...@@ -398,7 +401,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -398,7 +401,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
self._do_renew(which='delete') self._do_renew(which='delete')
super(Instance, self).clean(*args, **kwargs) super(Instance, self).clean(*args, **kwargs)
def manual_state_change(self, new_state, reason=None, user=None): def manual_state_change(self, new_state="NOSTATE", reason=None, user=None):
""" Manually change state of an Instance.
Can be used to recover VM after administrator fixed problems.
"""
# TODO cancel concurrent activity (if exists) # TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(code_suffix='manual_state_change', act = InstanceActivity.create(code_suffix='manual_state_change',
instance=self, user=user) instance=self, user=user)
...@@ -536,6 +543,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -536,6 +543,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
proto=proto) proto=proto)
def get_connect_command(self, use_ipv6=False): def get_connect_command(self, use_ipv6=False):
"""Returns a formatted connect string.
"""
try: try:
port = self.get_connect_port(use_ipv6=use_ipv6) port = self.get_connect_port(use_ipv6=use_ipv6)
host = self.get_connect_host(use_ipv6=use_ipv6) host = self.get_connect_host(use_ipv6=use_ipv6)
...@@ -568,6 +577,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -568,6 +577,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
return return
def get_vm_desc(self): def get_vm_desc(self):
"""Serialize Instance object to vmdriver.
"""
return { return {
'name': self.vm_name, 'name': self.vm_name,
'vcpu': self.num_cores, 'vcpu': self.num_cores,
...@@ -951,7 +962,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -951,7 +962,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
def sleep(self, user=None, task_uuid=None, timeout=60): def sleep(self, user=None, task_uuid=None, timeout=60):
"""Suspend virtual machine with memory dump. """Suspend virtual machine with memory dump.
""" """
if self.state not in ['RUNNING']: if self.status not in ['RUNNING']:
raise self.WrongStateError(self) raise self.WrongStateError(self)
def __on_abort(activity, error): def __on_abort(activity, error):
...@@ -989,7 +1000,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -989,7 +1000,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
queue="localhost.man") queue="localhost.man")
def wake_up(self, user=None, task_uuid=None, timeout=60): def wake_up(self, user=None, task_uuid=None, timeout=60):
if self.state not in ['SUSPENDED']: """ Wake up Virtual Machine from SUSPENDED state.
Power on Virtual Machine and load its memory from dump.
"""
if self.status not in ['SUSPENDED']:
raise self.WrongStateError(self) raise self.WrongStateError(self)
def __on_abort(activity, error): def __on_abort(activity, error):
...@@ -1125,28 +1140,38 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -1125,28 +1140,38 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
net.deploy() net.deploy()
def save_as_template_async(self, name, user=None, **kwargs): def save_as_template_async(self, name, user=None, **kwargs):
""" Save as template asynchronusly.
"""
return local_tasks.save_as_template.apply_async( return local_tasks.save_as_template.apply_async(
args=[self, name, user, kwargs], queue="localhost.man") args=[self, name, user, kwargs], queue="localhost.man")
def save_as_template(self, name, task_uuid=None, user=None, def save_as_template(self, name, task_uuid=None, user=None,
timeout=300, **kwargs): timeout=300, **kwargs):
""" Save Virtual Machine as a Template.
Template can be shared with groups and users.
Users can instantiate Virtual Machines from Templates.
"""
with instance_activity(code_suffix="save_as_template", instance=self, with instance_activity(code_suffix="save_as_template", instance=self,
task_uuid=task_uuid, user=user) as act: task_uuid=task_uuid, user=user) as act:
# prepare parameters # prepare parameters
kwargs.setdefault('name', name) params = {
kwargs.setdefault('description', self.description) 'access_method': self.access_method,
kwargs.setdefault('parent', self.template) 'arch': self.arch,
kwargs.setdefault('num_cores', self.num_cores) 'boot_menu': self.boot_menu,
kwargs.setdefault('ram_size', self.ram_size) 'description': self.description,
kwargs.setdefault('max_ram_size', self.max_ram_size) 'lease': self.lease, # Can be problem in new VM
kwargs.setdefault('arch', self.arch) 'max_ram_size': self.max_ram_size,
kwargs.setdefault('priority', self.priority) 'name': name,
kwargs.setdefault('boot_menu', self.boot_menu) 'num_cores': self.num_cores,
kwargs.setdefault('raw_data', self.raw_data) 'owner': user,
kwargs.setdefault('lease', self.lease) 'parent': self.template, # Can be problem
kwargs.setdefault('access_method', self.access_method) 'priority': self.priority,
kwargs.setdefault('system', self.template.system 'ram_size': self.ram_size,
if self.template else None) 'raw_data': self.raw_data,
'system': self.system,
}
params.update(kwargs)
def __try_save_disk(disk): def __try_save_disk(disk):
try: try:
...@@ -1155,15 +1180,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -1155,15 +1180,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
return disk return disk
# create template and do additional setup # create template and do additional setup
tmpl = InstanceTemplate(**kwargs) tmpl = InstanceTemplate(**params)
tmpl.full_clean() # Avoiding database errors. tmpl.full_clean() # Avoiding database errors.
tmpl.save() 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: try:
with act.sub_activity('saving_disks'):
tmpl.disks.add(*[__try_save_disk(disk)
for disk in self.disks.all()])
# create interface templates # create interface templates
for i in self.interface_set.all(): for i in self.interface_set.all():
i.save_as_template(tmpl) i.save_as_template(tmpl)
......
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