Commit 5912d4e8 by Kálmán Viktor

Merge branch 'master' into feature-template-wizard

Conflicts:
	circle/dashboard/urls.py
	requirements/local.txt
parents 0a56f429 c7b205a4
...@@ -177,14 +177,15 @@ class AclBase(Model): ...@@ -177,14 +177,15 @@ class AclBase(Model):
@classmethod @classmethod
def get_objects_with_level(cls, level, user, def get_objects_with_level(cls, level, user,
group_also=True, owner_also=False): group_also=True, owner_also=False,
disregard_superuser=False):
logger.debug('%s.get_objects_with_level(%s,%s) called', logger.debug('%s.get_objects_with_level(%s,%s) called',
unicode(cls), unicode(level), unicode(user)) unicode(cls), unicode(level), unicode(user))
if user is None or not user.is_authenticated(): if user is None or not user.is_authenticated():
return cls.objects.none() return cls.objects.none()
if getattr(user, 'is_superuser', False): if getattr(user, 'is_superuser', False) and not disregard_superuser:
logger.debug('- superuser granted') logger.debug('- superuser granted')
return cls.objects return cls.objects.all()
if isinstance(level, basestring): if isinstance(level, basestring):
level = cls.get_level_object(level) level = cls.get_level_object(level)
logger.debug("- level set by str: %s", unicode(level)) logger.debug("- level set by str: %s", unicode(level))
......
...@@ -161,6 +161,20 @@ class AclUserTest(TestCase): ...@@ -161,6 +161,20 @@ class AclUserTest(TestCase):
self.assertItemsEqual( self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.u2), [i2]) TestModel.get_objects_with_level('alfa', self.u2), [i2])
def test_get_objects_with_level_for_superuser(self):
i1 = TestModel.objects.create(normal_field='Hello1')
i2 = TestModel.objects.create(normal_field='Hello2')
i1.set_level(self.u1, 'alfa')
i2.set_level(self.us, 'alfa')
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.u1), [i1])
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.us), [i1, i2])
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.us,
disregard_superuser=True), [i2])
def test_get_objects_with_level_for_group(self): def test_get_objects_with_level_for_group(self):
i1 = TestModel.objects.create(normal_field='Hello1') i1 = TestModel.objects.create(normal_field='Hello1')
i2 = TestModel.objects.create(normal_field='Hello2') i2 = TestModel.objects.create(normal_field='Hello2')
......
from base import * # noqa from .base import * # noqa
########## TEST SETTINGS
TEST_RUNNER = 'discover_runner.DiscoverRunner'
TEST_DISCOVER_TOP_LEVEL = SITE_ROOT
TEST_DISCOVER_ROOT = SITE_ROOT
TEST_DISCOVER_PATTERN = "test_*.py"
########## IN-MEMORY TEST DATABASE ########## IN-MEMORY TEST DATABASE
DATABASES = { DATABASES = {
"default": { "default": {
...@@ -21,7 +16,6 @@ SOUTH_TESTS_MIGRATE = False ...@@ -21,7 +16,6 @@ SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += ( INSTALLED_APPS += (
'acl.tests', 'acl.tests',
'django_nose',
) )
CACHES = { CACHES = {
......
...@@ -62,8 +62,9 @@ class Operation(object): ...@@ -62,8 +62,9 @@ class Operation(object):
For more information, check the synchronous call's documentation. For more information, check the synchronous call's documentation.
""" """
logger.info("%s called asynchronously with the following parameters: " logger.info("%s called asynchronously on %s with the following "
"%r", self.__class__.__name__, kwargs) "parameters: %r", self.__class__.__name__, self.subject,
kwargs)
activity = self.__prelude(kwargs) activity = self.__prelude(kwargs)
return self.async_operation.apply_async(args=(self.id, return self.async_operation.apply_async(args=(self.id,
self.subject.pk, self.subject.pk,
...@@ -84,8 +85,9 @@ class Operation(object): ...@@ -84,8 +85,9 @@ class Operation(object):
* user: The User invoking the operation. If this argument is not * user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None. present, it'll be provided with a default value of None.
""" """
logger.info("%s called (synchronously) with the following parameters: " logger.info("%s called (synchronously) on %s with the following "
"%r", self.__class__.__name__, kwargs) "parameters: %r", self.__class__.__name__, self.subject,
kwargs)
activity = self.__prelude(kwargs) activity = self.__prelude(kwargs)
return self._exec_op(activity=activity, **kwargs) return self._exec_op(activity=activity, **kwargs)
......
...@@ -187,7 +187,7 @@ html { ...@@ -187,7 +187,7 @@ html {
height: 300px; height: 300px;
} }
#vm-details-rename, #vm-details-rename *, #vm-details-h1-name, #vm-list-rename, #vm-list-rename *, #vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
#node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename * { #node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename * {
display: inline; display: inline;
...@@ -197,7 +197,11 @@ html { ...@@ -197,7 +197,11 @@ html {
display: none; display: none;
} }
#vm-details-rename-name, #node-details-rename-name, #group-details-rename-name { .vm-details-home-name {
max-width: 401px;
}
#node-details-rename-name, #group-details-rename-name {
max-width: 160px; max-width: 160px;
} }
...@@ -516,3 +520,23 @@ footer a, footer a:hover, footer a:visited { ...@@ -516,3 +520,23 @@ footer a, footer a:hover, footer a:visited {
overflow: hidden; overflow: hidden;
padding-left: 10px; padding-left: 10px;
} }
#vm-details-home-description {
display: inline-block;
position: relative;
}
#vm-details-home-description textarea {
min-width: 240px;
min-height: 250px;
}
.vm-details-home-edit-description-click, .vm-details-home-edit-name-click {
cursor: pointer;
}
.vm-details-description-submit {
position: absolute;
bottom: 10px;
right: 20px;
}
...@@ -321,7 +321,8 @@ function deleteObject(data) { ...@@ -321,7 +321,8 @@ function deleteObject(data) {
// no need to remove them from DOM // no need to remove them from DOM
$('a[data-disk-pk="' + data.pk + '"]').parent("li").fadeOut(); $('a[data-disk-pk="' + data.pk + '"]').parent("li").fadeOut();
$('a[data-disk-pk="' + data.pk + '"]').parent("h4").fadeOut(); $('a[data-disk-pk="' + data.pk + '"]').parent("h4").fadeOut();
} else { }
else {
$('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() { $('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() {
$(this).remove(); $(this).remove();
}); });
......
...@@ -31,33 +31,6 @@ $(function() { ...@@ -31,33 +31,6 @@ $(function() {
return false; return false;
}); });
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename ajax */
$('#vm-details-rename-submit').click(function() {
var name = $('#vm-details-rename-name').val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-details-h1-name").html(data['new_name']).show();
$('#vm-details-rename').hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* remove tag */ /* remove tag */
$('.vm-details-remove-tag').click(function() { $('.vm-details-remove-tag').click(function() {
var to_remove = $.trim($(this).parent('div').text()); var to_remove = $.trim($(this).parent('div').text());
...@@ -150,8 +123,7 @@ $(function() { ...@@ -150,8 +123,7 @@ $(function() {
/* add network button */ /* add network button */
$("#vm-details-network-add").click(function() { $("#vm-details-network-add").click(function() {
$("#vm-details-network-add-for-form").html($("#vm-details-network-add-form").html()); $("#vm-details-network-add-form").toggle();
$('input[name="new_network_managed"]').tooltip();
return false; return false;
}); });
...@@ -165,6 +137,126 @@ $(function() { ...@@ -165,6 +137,126 @@ $(function() {
$(".vm-details-help-button").click(function() { $(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle(); $(".vm-details-help").stop().slideToggle();
}); });
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
addModalConfirmation(removeInterface,
{ 'url': '/dashboard/interface/' + interface_pk + '/delete/',
'data': [],
'pk': interface_pk,
'type': "interface",
});
return false;
});
/* removing interface post */
function removeInterface(data) {
$.ajax({
type: 'POST',
url: data['url'],
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "": "") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger')
}
});
}
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename in home tab */
$(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus();
});
/* rename ajax */
$('.vm-details-rename-submit').click(function() {
var name = $(this).parent("span").prev("input").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$(".vm-details-home-edit-name").text(data['new_name']).show();
$(".vm-details-home-edit-name").parent("div").show();
$(".vm-details-home-edit-name-click").show();
$(".vm-details-home-rename-form-div").hide();
// update the inputs too
$(".vm-details-rename-submit").parent("span").prev("input").val(data['new_name']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* update description click */
$(".vm-details-home-edit-description-click").click(function() {
$(".vm-details-home-edit-description-click").hide();
$("#vm-details-home-description").show();
return false;
});
/* description update ajax */
$('.vm-details-description-submit').click(function() {
var description = $(this).prev("textarea").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_description': description},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
var new_desc = data['new_description'];
/* we can't simply use $.text, because we need new lines */
var tagsToReplace = {
'&': "&amp;",
'<': "&lt;",
'>': "&gt;",
};
new_desc = new_desc.replace(/[&<>]/g, function(tag) {
return tagsToReplace[tag] || tag;
});
$(".vm-details-home-edit-description")
.html(new_desc.replace(/\n/g, "<br />"));
$(".vm-details-home-edit-description-click").show();
$("#vm-details-home-description").hide();
// update the textareia
$("vm-details-home-description textarea").text(data['new_description']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
}); });
......
...@@ -36,14 +36,18 @@ ...@@ -36,14 +36,18 @@
{% include "dashboard/vm-detail/_operations.html" %} {% include "dashboard/vm-detail/_operations.html" %}
</div> </div>
<h1> <h1>
<div id="vm-details-rename"> <div id="vm-details-rename" class="vm-details-home-rename-form-div">
<form action="" method="POST" id="vm-details-rename-form"> <form action="" method="POST" id="vm-details-rename-form">
{% csrf_token %} {% csrf_token %}
<input id="vm-details-rename-name" class="form-control" name="new_name" type="text" value="{{ instance.name }}"/> <div class="input-group vm-details-home-name">
<button type="submit" id="vm-details-rename-submit" class="btn">{% trans "Rename" %}</button> <input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-sm vm-details-rename-submit">{% trans "Rename" %}</button>
</span>
</div>
</form> </form>
</div> </div>
<div id="vm-details-h1-name"> <div id="vm-details-h1-name" class="vm-details-home-edit-name">
{{ instance.name }} {{ instance.name }}
</div> </div>
<small>{{ instance.primary_host.get_fqdn }}</small> <small>{{ instance.primary_host.get_fqdn }}</small>
......
...@@ -4,8 +4,46 @@ ...@@ -4,8 +4,46 @@
<dl> <dl>
<dt>{% trans "System" %}:</dt> <dt>{% trans "System" %}:</dt>
<dd><i class="icon-{{ os_type_icon }}"></i> {{ instance.system }}</dd> <dd><i class="icon-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;">{% trans "Description" %}:</dt> <dt style="margin-top: 5px;">
<dd><small>{{ instance.description }}</small></dd> {% trans "Name" %}:
<a href="#" class="vm-details-home-edit-name-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
<div class="vm-details-home-edit-name-click">
<small class="vm-details-home-edit-name">{{ instance.name }}</small>
</div>
<div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename">
<form method="POST">
{% csrf_token %}
<div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm vm-details-rename-submit">
<i class="icon-pencil"></i> {% trans "Rename" %}
</button>
</span>
</div>
</form>
</div>
</dd>
<dt style="margin-top: 5px;">
{% trans "Description" %}:
<a href="#" class="vm-details-home-edit-description-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
{% csrf_token %}
<div class="vm-details-home-edit-description-click">
<small class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</small>
</div>
<div id="vm-details-home-description" class="js-hidden">
<form method="POST">
<textarea name="new_description" class="form-control">{{ instance.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success vm-details-description-submit">
<i class="icon-pencil"></i> {% trans "Update" %}
</button>
</form>
</div>
</dd>
</dl> </dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %} <h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
......
...@@ -6,13 +6,50 @@ ...@@ -6,13 +6,50 @@
{% trans "Interfaces" %} {% trans "Interfaces" %}
</h2> </h2>
<div class="row" id="vm-details-network-add-for-form"> <div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control font-awesome-font">
{% for v in vlans %}
<option value="{{ v.pk }}">
{% if v.managed %}
&#xf0ac;
{% else %}
&#xf0c1;
{% endif %}
{{ v.name }}
</option>
{% empty %}
<option value="-1">No more networks!</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button {% if vlans|length == 0 %}disabled{% endif %}
type="submit" class="btn btn-success"><i class="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div> </div>
{% for i in instance.interface_set.all %} {% for i in instance.interface_set.all %}
<div>
<h3 class="list-group-item-heading dashboard-vm-details-network-h3"> <h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }} <i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host %}(unmanaged) <a href="#" class="btn btn-danger btn-xs">{% trans "remove" %}</a>{% endif %} {% if not i.host%}({% trans "unmanaged" %}){% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" class="btn btn-danger btn-xs interface-remove"
data-interface-pk="{{ i.pk }}">
{% trans "remove" %}
</a>
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
...@@ -109,31 +146,5 @@ ...@@ -109,31 +146,5 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endfor %}
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control">
{% if vlans|length == 0 %}
<option value="-1">No more networks!</option>
{% endif %}
{% for v in vlans %}
<option value="{{ v.pk }}">{{ v.name }}</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button type="submit" class="btn btn-success"><i class="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div> </div>
{% endfor %}
import json
from unittest import skip from unittest import skip
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
...@@ -174,6 +175,46 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -174,6 +175,46 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(inst.interface_set.count(), interface_count + 1) self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
c.post("/dashboard/interface/1/delete/")
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_permitted_network_delete_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
vlan = Vlan.objects.get(pk=1)
inst.add_interface(vlan=vlan, user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/",
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
removed_network = json.loads(response.content)['removed_network']
self.assertEqual(removed_network['vlan'], vlan.name)
self.assertEqual(removed_network['vlan_pk'], vlan.pk)
self.assertEqual(removed_network['managed'], vlan.managed)
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_unpermitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'user')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/")
self.assertEqual(iface_count, inst.interface_set.count())
self.assertEqual(response.status_code, 403)
def test_create_vm_w_unpermitted_network(self): def test_create_vm_w_unpermitted_network(self):
c = Client() c = Client()
self.login(c, 'user2') self.login(c, 'user2')
...@@ -559,6 +600,41 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -559,6 +600,41 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(instance_count + 2, Instance.objects.all().count()) self.assertEqual(instance_count + 2, Instance.objects.all().count())
def test_unpermitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.set_level(self.u1, 'user')
old_desc = inst.description
response = c.post("/dashboard/vm/1/", {'new_description': 'test1234'})
self.assertEqual(response.status_code, 403)
self.assertEqual(Instance.objects.get(pk=1).description, old_desc)
def test_permitted_description_update_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['new_description'],
"naonyo")
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
def test_permitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"})
self.assertEqual(response.status_code, 302)
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
class NodeDetailTest(LoginMixin, TestCase): class NodeDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
...@@ -820,8 +896,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): ...@@ -820,8 +896,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
c2 = self.u2.notification_set.count() c2 = self.u2.notification_set.count()
c = Client() c = Client()
self.login(c, 'user2') self.login(c, 'user2')
with self.assertRaises(SuspiciousOperation): response = c.post('/dashboard/vm/1/tx/')
c.post('/dashboard/vm/1/tx/') assert response.status_code == 400
self.assertEqual(self.u2.notification_set.count(), c2) self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self): def test_owned_offer(self):
......
...@@ -12,7 +12,7 @@ from .views import ( ...@@ -12,7 +12,7 @@ from .views import (
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status, VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
TemplateChoose, TemplateClone, TemplateChoose, TemplateClone,
) )
...@@ -114,6 +114,9 @@ urlpatterns = patterns( ...@@ -114,6 +114,9 @@ urlpatterns = patterns(
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status, url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"), name="dashboard.views.disk-status"),
url(r'^interface/(?P<pk>\d+)/delete/$', InterfaceDeleteView.as_view(),
name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(), url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile"), name="dashboard.views.profile"),
) )
...@@ -30,9 +30,8 @@ from django.template import RequestContext ...@@ -30,9 +30,8 @@ from django.template import RequestContext
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from braces.views import ( from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
LoginRequiredMixin, SuperuserRequiredMixin, AccessMixin from braces.views._access import AccessMixin
)
from .forms import ( from .forms import (
CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm, CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
...@@ -90,7 +89,7 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -90,7 +89,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
# instances # instances
favs = Instance.objects.filter(favourite__user=self.request.user) favs = Instance.objects.filter(favourite__user=self.request.user)
instances = Instance.get_objects_with_level( instances = Instance.get_objects_with_level(
'user', user).filter(destroyed_at=None) 'user', user, disregard_superuser=True).filter(destroyed_at=None)
display = list(favs) + list(set(instances) - set(favs)) display = list(favs) + list(set(instances) - set(favs))
for d in display: for d in display:
d.fav = True if d in favs else False d.fav = True if d in favs else False
...@@ -216,7 +215,7 @@ class VmDetailView(CheckedDetailView): ...@@ -216,7 +215,7 @@ class VmDetailView(CheckedDetailView):
context['vlans'] = Vlan.get_objects_with_level( context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user 'user', self.request.user
).exclude( ).exclude( # exclude already added interfaces
pk__in=Interface.objects.filter( pk__in=Interface.objects.filter(
instance=self.get_object()).values_list("vlan", flat=True) instance=self.get_object()).values_list("vlan", flat=True)
).all() ).all()
...@@ -239,6 +238,7 @@ class VmDetailView(CheckedDetailView): ...@@ -239,6 +238,7 @@ class VmDetailView(CheckedDetailView):
options = { options = {
'change_password': self.__change_password, 'change_password': self.__change_password,
'new_name': self.__set_name, 'new_name': self.__set_name,
'new_description': self.__set_description,
'new_tag': self.__add_tag, 'new_tag': self.__add_tag,
'to_remove': self.__remove_tag, 'to_remove': self.__remove_tag,
'port': self.__add_port, 'port': self.__add_port,
...@@ -309,8 +309,30 @@ class VmDetailView(CheckedDetailView): ...@@ -309,8 +309,30 @@ class VmDetailView(CheckedDetailView):
) )
else: else:
messages.success(request, success_message) messages.success(request, success_message)
return redirect(reverse_lazy("dashboard.views.detail", return redirect(self.object.get_absolute_url())
kwargs={'pk': self.object.pk}))
def __set_description(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
new_description = request.POST.get("new_description")
Instance.objects.filter(pk=self.object.pk).update(
**{'description': new_description})
success_message = _("VM description successfully updated!")
if request.is_ajax():
response = {
'message': success_message,
'new_description': new_description,
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __add_tag(self, request): def __add_tag(self, request):
new_tag = request.POST.get('new_tag') new_tag = request.POST.get('new_tag')
...@@ -393,7 +415,7 @@ class VmDetailView(CheckedDetailView): ...@@ -393,7 +415,7 @@ class VmDetailView(CheckedDetailView):
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
vlan = Vlan.objects.get(pk=request.POST.get("new_network_vlan")) vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
if not vlan.has_level(request.user, 'user'): if not vlan.has_level(request.user, 'user'):
raise PermissionDenied() raise PermissionDenied()
try: try:
...@@ -481,6 +503,7 @@ class OperationView(DetailView): ...@@ -481,6 +503,7 @@ class OperationView(DetailView):
class VmOperationView(OperationView): class VmOperationView(OperationView):
model = Instance model = Instance
context_object_name = 'instance' # much simpler to mock object
class VmMigrateView(VmOperationView): class VmMigrateView(VmOperationView):
...@@ -2353,6 +2376,7 @@ def get_disk_download_status(request, pk): ...@@ -2353,6 +2376,7 @@ def get_disk_download_status(request, pk):
class InstanceActivityDetail(SuperuserRequiredMixin, DetailView): class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
model = InstanceActivity model = InstanceActivity
context_object_name = 'instanceactivity' # much simpler to mock object
template_name = 'dashboard/instanceactivity_detail.html' template_name = 'dashboard/instanceactivity_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
...@@ -2362,3 +2386,53 @@ class InstanceActivityDetail(SuperuserRequiredMixin, DetailView): ...@@ -2362,3 +2386,53 @@ class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
order_by('-started').select_related('user'). order_by('-started').select_related('user').
prefetch_related('children')) prefetch_related('children'))
return ctx return ctx
class InterfaceDeleteView(DeleteView):
model = Interface
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(InterfaceDeleteView, self).get_context_data(**kwargs)
interface = self.get_object()
context['text'] = _("Are you sure you want to remove this interface "
"from <strong>%(vm)s</strong>?" %
{'vm': interface.instance.name})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
instance = self.object.instance
if not instance.has_level(request.user, "owner"):
raise PermissionDenied()
instance.remove_interface(interface=self.object, user=request.user)
success_url = self.get_success_url()
success_message = _("Interface successfully deleted!")
if request.is_ajax():
return HttpResponse(
json.dumps(
{'message': success_message,
'removed_network': {
'vlan': self.object.vlan.name,
'vlan_pk': self.object.vlan.pk,
'managed': self.object.host is not None,
}}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
redirect = self.request.POST.get("next")
if redirect:
return redirect
self.object.instance.get_absolute_url()
...@@ -2,7 +2,7 @@ import re ...@@ -2,7 +2,7 @@ import re
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from netaddr import IPAddress, AddrFormatError from netaddr import IPAddress, AddrFormatError
from datetime import datetime, timedelta from datetime import timedelta
from itertools import product from itertools import product
from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem, from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
...@@ -11,6 +11,7 @@ from .iptables import IptRule, IptChain ...@@ -11,6 +11,7 @@ from .iptables import IptRule, IptChain
import django.conf import django.conf
from django.db.models import Q from django.db.models import Q
from django.template import loader, Context from django.template import loader, Context
from django.utils import timezone
settings = django.conf.settings.FIREWALL_SETTINGS settings = django.conf.settings.FIREWALL_SETTINGS
...@@ -134,7 +135,7 @@ class BuildFirewall: ...@@ -134,7 +135,7 @@ class BuildFirewall:
def ipset(): def ipset():
week = datetime.now() - timedelta(days=2) week = timezone.now() - timedelta(days=2)
filter_ban = (Q(type='tempban', modified_at__gte=week) | filter_ban = (Q(type='tempban', modified_at__gte=week) |
Q(type='permban')) Q(type='permban'))
return BlacklistItem.objects.filter(filter_ban).values('ipv4', 'reason') return BlacklistItem.objects.filter(filter_ban).values('ipv4', 'reason')
......
...@@ -52,6 +52,8 @@ ...@@ -52,6 +52,8 @@
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
{% include "network/menu.html" %} {% include "network/menu.html" %}
</ul> </ul>
<a class="navbar-brand pull-right" href="{% url "dashboard.index" %}"
style="color: white; font-size: 10px;"><i class="icon-dashboard"></i> {% trans "dashboard" %}</a>
</div><!-- .collapse .navbar-collapse --> </div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top --> </div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <div class="container">
...@@ -75,7 +77,7 @@ ...@@ -75,7 +77,7 @@
<div class="footer-container container"> <div class="footer-container container">
<footer> <footer>
<p class="pull-right"><a href="#">Vissza az oldal tetejére</a></p> <p class="pull-right"><a href="#">Vissza az oldal tetejére</a></p>
<p>&copy; 2013 BME Közigazgatási Informatikai Központ <p>&copy; {{ COMPANY_NAME }}
</footer> </footer>
</div><!-- .footer-container .container --> </div><!-- .footer-container .container -->
......
...@@ -46,18 +46,13 @@ def restore(disk, user): ...@@ -46,18 +46,13 @@ def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=user) disk.restore(task_uuid=restore.request.id, user=user)
class CreateFromURLTask(AbortableTask): @celery.task(base=AbortableTask, bind=True)
def create_from_url(self, **kwargs):
def __init__(self): Disk = kwargs.pop('cls')
self.bind(celery) Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=self.request.id,
def run(self, **kwargs): abortable_task=self,
Disk = kwargs.pop('cls') **kwargs)
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=create_from_url.request.id,
abortable_task=self,
**kwargs)
create_from_url = CreateFromURLTask()
@celery.task @celery.task
......
...@@ -53,6 +53,7 @@ class Interface(Model): ...@@ -53,6 +53,7 @@ class Interface(Model):
class Meta: class Meta:
app_label = 'vm' app_label = 'vm'
db_table = 'vm_interface' db_table = 'vm_interface'
ordering = ("-vlan__managed", )
def __unicode__(self): def __unicode__(self):
return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid) return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)
......
...@@ -85,7 +85,6 @@ class DeployOperation(InstanceOperation): ...@@ -85,7 +85,6 @@ class DeployOperation(InstanceOperation):
id = 'deploy' id = 'deploy'
name = _("deploy") name = _("deploy")
description = _("Deploy new virtual machine with network.") description = _("Deploy new virtual machine with network.")
icon = 'play'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'RUNNING' activity.resultant_state = 'RUNNING'
...@@ -122,7 +121,6 @@ class DestroyOperation(InstanceOperation): ...@@ -122,7 +121,6 @@ class DestroyOperation(InstanceOperation):
id = 'destroy' id = 'destroy'
name = _("destroy") name = _("destroy")
description = _("Destroy virtual machine and its networks.") description = _("Destroy virtual machine and its networks.")
icon = 'remove'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'DESTROYED' activity.resultant_state = 'DESTROYED'
...@@ -164,7 +162,6 @@ class MigrateOperation(InstanceOperation): ...@@ -164,7 +162,6 @@ class MigrateOperation(InstanceOperation):
id = 'migrate' id = 'migrate'
name = _("migrate") name = _("migrate")
description = _("Live migrate running VM to another node.") description = _("Live migrate running VM to another node.")
icon = 'truck'
def _operation(self, activity, user, system, to_node=None, timeout=120): def _operation(self, activity, user, system, to_node=None, timeout=120):
if not to_node: if not to_node:
...@@ -195,7 +192,6 @@ class RebootOperation(InstanceOperation): ...@@ -195,7 +192,6 @@ class RebootOperation(InstanceOperation):
id = 'reboot' id = 'reboot'
name = _("reboot") name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.") description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
icon = 'refresh'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reboot_vm(timeout=timeout) self.instance.reboot_vm(timeout=timeout)
...@@ -226,7 +222,6 @@ class ResetOperation(InstanceOperation): ...@@ -226,7 +222,6 @@ class ResetOperation(InstanceOperation):
id = 'reset' id = 'reset'
name = _("reset") name = _("reset")
description = _("Reset virtual machine (reset button).") description = _("Reset virtual machine (reset button).")
icon = 'bolt'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reset_vm(timeout=timeout) self.instance.reset_vm(timeout=timeout)
...@@ -243,7 +238,6 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -243,7 +238,6 @@ class SaveAsTemplateOperation(InstanceOperation):
Template can be shared with groups and users. Template can be shared with groups and users.
Users can instantiate Virtual Machines from Templates. Users can instantiate Virtual Machines from Templates.
""") """)
icon = 'save'
@staticmethod @staticmethod
def _rename(name): def _rename(name):
...@@ -319,7 +313,6 @@ class ShutdownOperation(InstanceOperation): ...@@ -319,7 +313,6 @@ class ShutdownOperation(InstanceOperation):
id = 'shutdown' id = 'shutdown'
name = _("shutdown") name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.") description = _("Shutdown virtual machine with ACPI signal.")
icon = 'off'
def check_precond(self): def check_precond(self):
super(ShutdownOperation, self).check_precond() super(ShutdownOperation, self).check_precond()
...@@ -349,7 +342,6 @@ class ShutOffOperation(InstanceOperation): ...@@ -349,7 +342,6 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off' id = 'shut_off'
name = _("shut off") name = _("shut off")
description = _("Shut off VM (plug-out).") description = _("Shut off VM (plug-out).")
icon = 'ban-circle'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'STOPPED' activity.resultant_state = 'STOPPED'
...@@ -376,7 +368,6 @@ class SleepOperation(InstanceOperation): ...@@ -376,7 +368,6 @@ class SleepOperation(InstanceOperation):
id = 'sleep' id = 'sleep'
name = _("sleep") name = _("sleep")
description = _("Suspend virtual machine with memory dump.") description = _("Suspend virtual machine with memory dump.")
icon = 'moon'
def check_precond(self): def check_precond(self):
super(SleepOperation, self).check_precond() super(SleepOperation, self).check_precond()
...@@ -416,7 +407,6 @@ class WakeUpOperation(InstanceOperation): ...@@ -416,7 +407,6 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump. Power on Virtual Machine and load its memory from dump.
""") """)
icon = 'sun'
def check_precond(self): def check_precond(self):
super(WakeUpOperation, self).check_precond() super(WakeUpOperation, self).check_precond()
......
from datetime import datetime from datetime import datetime
from mock import Mock, MagicMock, patch, call from mock import Mock, MagicMock, patch, call
import types
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
...@@ -180,14 +181,18 @@ class InstanceActivityTestCase(TestCase): ...@@ -180,14 +181,18 @@ class InstanceActivityTestCase(TestCase):
def test_create_no_concurrency_check(self): def test_create_no_concurrency_check(self):
instance = MagicMock(spec=Instance) instance = MagicMock(spec=Instance)
instance.activity_log.filter.return_value.exists.return_value = True instance.activity_log.filter.return_value.exists.return_value = True
mock_instance_activity_cls = MagicMock(spec=InstanceActivity,
with patch.object(InstanceActivity, '__new__'): ACTIVITY_CODE_BASE='test')
try:
InstanceActivity.create('test', instance, original_create = InstanceActivity.create
concurrency_check=False) mocked_create = types.MethodType(original_create.im_func,
except ActivityInProgressError: mock_instance_activity_cls,
raise AssertionError("'create' method checked for concurrent " original_create.im_class)
"activities.") try:
mocked_create('test', instance, concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create' method checked for concurrent "
"activities.")
def test_create_sub_concurrency_check(self): def test_create_sub_concurrency_check(self):
iaobj = MagicMock(spec=InstanceActivity) iaobj = MagicMock(spec=InstanceActivity)
...@@ -201,10 +206,10 @@ class InstanceActivityTestCase(TestCase): ...@@ -201,10 +206,10 @@ class InstanceActivityTestCase(TestCase):
iaobj.activity_code = 'test' iaobj.activity_code = 'test'
iaobj.children.filter.return_value.exists.return_value = True iaobj.children.filter.return_value.exists.return_value = True
with patch.object(InstanceActivity, '__new__'): create_sub_func = InstanceActivity.create_sub
with patch('vm.models.activity.InstanceActivity'):
try: try:
InstanceActivity.create_sub(iaobj, 'test', create_sub_func(iaobj, 'test', concurrency_check=False)
concurrency_check=False)
except ActivityInProgressError: except ActivityInProgressError:
raise AssertionError("'create_sub' method checked for " raise AssertionError("'create_sub' method checked for "
"concurrent activities.") "concurrent activities.")
......
# This file is here because many Platforms as a Service look for # File is here because many Platforms as a Service look for
# requirements.txt in the root directory of a project. # requirements.txt in the root directory of a project.
-r requirements/production.txt -r requirements/production.txt
Django==1.5.2 amqp==1.4.5
anyjson==0.3.3
billiard==3.3.0.17
bpython==0.12 bpython==0.12
celery==3.0.23 celery==3.1.11
django-braces==1.2.2 django-braces==1.4.0
django-celery==3.0.23 django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
django-model-utils==1.4.0 django-model-utils==2.0.3
django-sizefield==0.4 django-sizefield==0.4
django-tables2==0.14.0 django-tables2==0.15.0
django-taggit==0.11.2 django-taggit==0.12
Django==1.6.3
docutils==0.11
Jinja2==2.7.2
kombu==3.0.15
logutils==0.3.3 logutils==0.3.3
netaddr==0.7.10 MarkupSafe==0.21
netaddr==0.7.11
nose==1.3.1
pip-tools==0.3.4
psycopg2==2.5.2
Pygments==1.6
pylibmc==1.3.0
python-dateutil==2.2
pytz==2014.2
requests==2.2.1 requests==2.2.1
South==0.8.1 simplejson==3.4.0
psycopg2==2.5.1 six==1.6.1
pylibmc South==0.8.4
sqlparse==0.1.11
# Local development dependencies go here # Local development dependencies go here
-r base.txt -r base.txt
coverage==3.6 coverage==3.7.1
django-discover-runner==0.4 django-debug-toolbar==1.1
django-debug-toolbar==0.9.4 Sphinx==1.2.2
Sphinx==1.2b1
django-statici18n==1.1 django-statici18n==1.1
# Pro-tip: Try not to put anything here. There should be no dependency in # Pro-tip: Try not to put anything here. There should be no dependency in
# production that isn't in development. # production that isn't in development.
-r base.txt -r base.txt
uWSGI==2.0.4
gunicorn==0.17.4
uWSGI==2.0.3
# Test dependencies go here. # Test dependencies go here.
-r base.txt -r base.txt
coverage==3.6 coverage==3.7.1
django-discover-runner==0.4
django-nose==1.2
mock==1.0.1
factory-boy==2.3.1 factory-boy==2.3.1
mock==1.0.1
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